Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7bb49b1
Design shared action foundation
TerminallyLazy Jul 9, 2026
48d1049
Plan shared action foundation
TerminallyLazy Jul 9, 2026
6c75200
Ignore local worktrees
TerminallyLazy Jul 9, 2026
5ce7246
Fix shared action implementation plan
TerminallyLazy Jul 9, 2026
1a1d5d6
Add shared remember action
TerminallyLazy Jul 9, 2026
dc55530
Add shared scriptable actions
TerminallyLazy Jul 9, 2026
5b37f81
Wire scriptable CLI through shared actions
TerminallyLazy Jul 9, 2026
e8a9441
Use shared actions in TUI write flows
TerminallyLazy Jul 9, 2026
98dc641
Add lifecycle and integration actions
TerminallyLazy Jul 9, 2026
da0fefd
Split sqlite store internals
TerminallyLazy Jul 9, 2026
6ce09bc
Document shared action foundation
TerminallyLazy Jul 9, 2026
3d9f35d
Preserve TUI remember scope
TerminallyLazy Jul 9, 2026
6e12500
Refresh certification evidence
TerminallyLazy Jul 9, 2026
5a56d10
Design evidence spine roadmap
TerminallyLazy Jul 9, 2026
21cb7b9
Plan evidence spine phase one
TerminallyLazy Jul 9, 2026
2a7722c
Add evidence index reader
TerminallyLazy Jul 9, 2026
1520c9d
Fix evidence snapshot compatibility
TerminallyLazy Jul 9, 2026
d12b652
Format evidence reader
TerminallyLazy Jul 9, 2026
57fb5ff
Write certification evidence index
TerminallyLazy Jul 9, 2026
dfd696c
Add TUI evidence state
TerminallyLazy Jul 9, 2026
9e72f97
Render evidence browser in TUI
TerminallyLazy Jul 9, 2026
8090ba4
Format TUI evidence code
TerminallyLazy Jul 9, 2026
7756201
Document evidence spine phase one
TerminallyLazy Jul 9, 2026
a753258
Remove SDD scratch reports
TerminallyLazy Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ build/
.tree-ring/
target/
outputs/
.worktrees/
bindings/python/python/tree_ring_memory/*.abi3.so
bindings/python/python/tree_ring_memory/*.so
bindings/python/python/tree_ring_memory/*.pyd
104 changes: 104 additions & 0 deletions crates/tree-ring-memory-cli/src/actions/adapters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::path::PathBuf;

use tree_ring_memory_core::{
collect_dox_memories, collect_revolve_memories, DoxSyncReport, DoxSyncRequest,
RevolveSyncReport, RevolveSyncRequest,
};
use tree_ring_memory_sqlite::SQLiteMemoryStore;

use super::ActionResult;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DoxSyncActionRequest {
pub source_root: PathBuf,
pub project: Option<String>,
pub dry_run: bool,
}

#[derive(Debug, Clone, PartialEq)]
pub struct DoxSyncActionReport {
pub report: DoxSyncReport,
pub dry_run: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RevolveSyncActionRequest {
pub source_root: PathBuf,
pub project: Option<String>,
pub dry_run: bool,
}

#[derive(Debug, Clone, PartialEq)]
pub struct RevolveSyncActionReport {
pub report: RevolveSyncReport,
pub dry_run: bool,
}

pub fn sync_dox(
store: Option<&mut SQLiteMemoryStore>,
request: DoxSyncActionRequest,
) -> ActionResult<DoxSyncActionReport> {
let mut dox_request = DoxSyncRequest::new(request.source_root);
dox_request.project = request.project;
let report = collect_dox_memories(&dox_request).map_err(|err| err.to_string())?;
if !request.dry_run {
let store = store.ok_or_else(|| {
"DOX sync action requires an open writable store when dry_run=false".to_string()
})?;
store
.put_many(&report.events)
.map_err(|err| err.to_string())?;
}
Ok(DoxSyncActionReport {
report,
dry_run: request.dry_run,
})
}

pub fn sync_revolve(
store: Option<&mut SQLiteMemoryStore>,
request: RevolveSyncActionRequest,
) -> ActionResult<RevolveSyncActionReport> {
let mut revolve_request = RevolveSyncRequest::new(request.source_root);
revolve_request.project = request.project;
let report = collect_revolve_memories(&revolve_request).map_err(|err| err.to_string())?;
if !request.dry_run {
let store = store.ok_or_else(|| {
"Revolve sync action requires an open writable store when dry_run=false".to_string()
})?;
store
.put_many(&report.events)
.map_err(|err| err.to_string())?;
}
Ok(RevolveSyncActionReport {
report,
dry_run: request.dry_run,
})
}

#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;

use super::*;

#[test]
fn dox_action_dry_run_does_not_write_events() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("AGENTS.md"), "# Rules\n\nAlways run tests.").unwrap();

let report = sync_dox(
None,
DoxSyncActionRequest {
source_root: dir.path().to_path_buf(),
project: Some("tree-ring".to_string()),
dry_run: true,
},
)
.unwrap();

assert_eq!(report.report.memory_count, 1);
assert!(!dir.path().join("memory.sqlite").exists());
}
}
51 changes: 51 additions & 0 deletions crates/tree-ring-memory-cli/src/actions/audit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::path::Path;

use tree_ring_memory_core::{audit_memories, AuditReport};
use tree_ring_memory_sqlite::SQLiteMemoryStore;

use super::ActionResult;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditActionRequest {
pub audit_type: String,
}

#[derive(Debug, Clone, PartialEq)]
pub struct AuditActionReport {
pub report: AuditReport,
}

pub fn audit_store(db_path: &Path, request: AuditActionRequest) -> ActionResult<AuditActionReport> {
let report = if db_path.exists() {
SQLiteMemoryStore::open_read_only(db_path)
.and_then(|store| store.audit(&request.audit_type))
.map_err(|err| err.to_string())?
} else {
audit_memories(&[], &request.audit_type).map_err(|err| err.to_string())?
};
Ok(AuditActionReport { report })
}

#[cfg(test)]
mod tests {
use tempfile::tempdir;

use super::*;

#[test]
fn audit_action_missing_store_reports_empty_audit_without_creating_store() {
let dir = tempdir().unwrap();
let db_path = dir.path().join(".tree-ring/memory.sqlite");

let report = audit_store(
&db_path,
AuditActionRequest {
audit_type: "all".to_string(),
},
)
.unwrap();

assert_eq!(report.report.memory_count, 0);
assert!(!db_path.exists());
}
}
160 changes: 160 additions & 0 deletions crates/tree-ring-memory-cli/src/actions/export_import.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use std::fs;
use std::path::PathBuf;

use serde_json::json;
use tree_ring_memory_core::{decode_jsonl, normalize_import_events};
use tree_ring_memory_sqlite::{ExportReport, ImportReport, SQLiteMemoryStore};

use super::ActionResult;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportActionRequest {
pub output: Option<PathBuf>,
pub include_sensitive: bool,
pub include_superseded: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportActionReport {
pub jsonl: Option<String>,
pub output: Option<PathBuf>,
pub report: ExportReport,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportActionRequest {
pub path: PathBuf,
pub dry_run: bool,
pub replace_existing: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportActionReport {
pub path: PathBuf,
pub report: ImportReport,
}

pub fn export_jsonl(
store: &SQLiteMemoryStore,
request: ExportActionRequest,
) -> ActionResult<ExportActionReport> {
let (jsonl, report) = store
.export_jsonl(request.include_sensitive, request.include_superseded)
.map_err(|err| err.to_string())?;
if let Some(output) = request.output {
if let Some(parent) = output.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent).map_err(|err| err.to_string())?;
}
}
fs::write(&output, jsonl).map_err(|err| err.to_string())?;
Ok(ExportActionReport {
jsonl: None,
output: Some(output),
report,
})
} else {
Ok(ExportActionReport {
jsonl: Some(jsonl),
output: None,
report,
})
}
}

pub fn import_jsonl(
store: Option<&mut SQLiteMemoryStore>,
request: ImportActionRequest,
) -> ActionResult<ImportActionReport> {
let input = fs::read_to_string(&request.path).map_err(|err| err.to_string())?;
let report = if request.dry_run {
let decoded = decode_jsonl(&input).map_err(|err| err.to_string())?;
let events = normalize_import_events(decoded.events).map_err(|err| err.to_string())?;
ImportReport {
valid_count: events.len(),
inserted_count: 0,
replaced_count: 0,
skipped_duplicate_count: 0,
dry_run: true,
}
} else {
let store = store.ok_or_else(|| {
"import action requires an open writable store when dry_run=false".to_string()
})?;
store
.import_jsonl(&input, false, request.replace_existing)
.map_err(|err| err.to_string())?
};
Ok(ImportActionReport {
path: request.path,
report,
})
}

pub fn import_json_payload(report: &ImportActionReport) -> serde_json::Value {
json!({
"ok": true,
"path": report.path,
"valid_count": report.report.valid_count,
"inserted_count": report.report.inserted_count,
"replaced_count": report.report.replaced_count,
"skipped_duplicate_count": report.report.skipped_duplicate_count,
"dry_run": report.report.dry_run,
})
}

#[cfg(test)]
mod tests {
use tempfile::tempdir;
use tree_ring_memory_core::MemoryEvent;

use super::*;

#[test]
fn export_action_can_return_stdout_jsonl() {
let dir = tempdir().unwrap();
let mut store = SQLiteMemoryStore::open(dir.path().join("memory.sqlite")).unwrap();
store
.put(&MemoryEvent::new("Export through shared action.", "lesson").unwrap())
.unwrap();

let report = export_jsonl(
&store,
ExportActionRequest {
output: None,
include_sensitive: false,
include_superseded: false,
},
)
.unwrap();

assert_eq!(report.report.memory_count, 1);
assert!(report.jsonl.unwrap().contains("memory_event"));
}

#[test]
fn import_action_dry_run_validates_without_writing() {
let dir = tempdir().unwrap();
let mut source = SQLiteMemoryStore::open(dir.path().join("source.sqlite")).unwrap();
let target = SQLiteMemoryStore::open(dir.path().join("target.sqlite")).unwrap();
source
.put(&MemoryEvent::new("Import through shared action.", "lesson").unwrap())
.unwrap();
let (jsonl, _) = source.export_jsonl(false, false).unwrap();
let input = dir.path().join("input.jsonl");
fs::write(&input, jsonl).unwrap();

let report = import_jsonl(
None,
ImportActionRequest {
path: input,
dry_run: true,
replace_existing: false,
},
)
.unwrap();

assert_eq!(report.report.valid_count, 1);
assert_eq!(target.list_all(true).unwrap().len(), 0);
}
}
39 changes: 39 additions & 0 deletions crates/tree-ring-memory-cli/src/actions/integrations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::path::PathBuf;

use crate::integrations::{scan_integrations, IntegrationScanReport};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IntegrationScanRequest {
pub source_root: PathBuf,
}

#[derive(Debug, Clone, PartialEq)]
pub struct IntegrationScanActionReport {
pub report: IntegrationScanReport,
}

pub fn scan(request: IntegrationScanRequest) -> IntegrationScanActionReport {
IntegrationScanActionReport {
report: scan_integrations(&request.source_root),
}
}

#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;

use super::*;

#[test]
fn integration_action_scans_project_markers() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("AGENTS.md"), "# Rules").unwrap();

let report = scan(IntegrationScanRequest {
source_root: dir.path().to_path_buf(),
});

assert!(report.report.detected_count > 0);
}
}
Loading
Loading