Skip to content

Commit 788ebcc

Browse files
Merge pull request #31 from TerminallyLazy/codex/evidence-spine-roadmap-design
Add evidence spine phase one
2 parents 7d00800 + a753258 commit 788ebcc

28 files changed

Lines changed: 5908 additions & 644 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ build/
1010
.tree-ring/
1111
target/
1212
outputs/
13+
.worktrees/
1314
bindings/python/python/tree_ring_memory/*.abi3.so
1415
bindings/python/python/tree_ring_memory/*.so
1516
bindings/python/python/tree_ring_memory/*.pyd
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::path::PathBuf;
2+
3+
use tree_ring_memory_core::{
4+
collect_dox_memories, collect_revolve_memories, DoxSyncReport, DoxSyncRequest,
5+
RevolveSyncReport, RevolveSyncRequest,
6+
};
7+
use tree_ring_memory_sqlite::SQLiteMemoryStore;
8+
9+
use super::ActionResult;
10+
11+
#[derive(Debug, Clone, PartialEq, Eq)]
12+
pub struct DoxSyncActionRequest {
13+
pub source_root: PathBuf,
14+
pub project: Option<String>,
15+
pub dry_run: bool,
16+
}
17+
18+
#[derive(Debug, Clone, PartialEq)]
19+
pub struct DoxSyncActionReport {
20+
pub report: DoxSyncReport,
21+
pub dry_run: bool,
22+
}
23+
24+
#[derive(Debug, Clone, PartialEq, Eq)]
25+
pub struct RevolveSyncActionRequest {
26+
pub source_root: PathBuf,
27+
pub project: Option<String>,
28+
pub dry_run: bool,
29+
}
30+
31+
#[derive(Debug, Clone, PartialEq)]
32+
pub struct RevolveSyncActionReport {
33+
pub report: RevolveSyncReport,
34+
pub dry_run: bool,
35+
}
36+
37+
pub fn sync_dox(
38+
store: Option<&mut SQLiteMemoryStore>,
39+
request: DoxSyncActionRequest,
40+
) -> ActionResult<DoxSyncActionReport> {
41+
let mut dox_request = DoxSyncRequest::new(request.source_root);
42+
dox_request.project = request.project;
43+
let report = collect_dox_memories(&dox_request).map_err(|err| err.to_string())?;
44+
if !request.dry_run {
45+
let store = store.ok_or_else(|| {
46+
"DOX sync action requires an open writable store when dry_run=false".to_string()
47+
})?;
48+
store
49+
.put_many(&report.events)
50+
.map_err(|err| err.to_string())?;
51+
}
52+
Ok(DoxSyncActionReport {
53+
report,
54+
dry_run: request.dry_run,
55+
})
56+
}
57+
58+
pub fn sync_revolve(
59+
store: Option<&mut SQLiteMemoryStore>,
60+
request: RevolveSyncActionRequest,
61+
) -> ActionResult<RevolveSyncActionReport> {
62+
let mut revolve_request = RevolveSyncRequest::new(request.source_root);
63+
revolve_request.project = request.project;
64+
let report = collect_revolve_memories(&revolve_request).map_err(|err| err.to_string())?;
65+
if !request.dry_run {
66+
let store = store.ok_or_else(|| {
67+
"Revolve sync action requires an open writable store when dry_run=false".to_string()
68+
})?;
69+
store
70+
.put_many(&report.events)
71+
.map_err(|err| err.to_string())?;
72+
}
73+
Ok(RevolveSyncActionReport {
74+
report,
75+
dry_run: request.dry_run,
76+
})
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use std::fs;
82+
use tempfile::tempdir;
83+
84+
use super::*;
85+
86+
#[test]
87+
fn dox_action_dry_run_does_not_write_events() {
88+
let dir = tempdir().unwrap();
89+
fs::write(dir.path().join("AGENTS.md"), "# Rules\n\nAlways run tests.").unwrap();
90+
91+
let report = sync_dox(
92+
None,
93+
DoxSyncActionRequest {
94+
source_root: dir.path().to_path_buf(),
95+
project: Some("tree-ring".to_string()),
96+
dry_run: true,
97+
},
98+
)
99+
.unwrap();
100+
101+
assert_eq!(report.report.memory_count, 1);
102+
assert!(!dir.path().join("memory.sqlite").exists());
103+
}
104+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use std::path::Path;
2+
3+
use tree_ring_memory_core::{audit_memories, AuditReport};
4+
use tree_ring_memory_sqlite::SQLiteMemoryStore;
5+
6+
use super::ActionResult;
7+
8+
#[derive(Debug, Clone, PartialEq, Eq)]
9+
pub struct AuditActionRequest {
10+
pub audit_type: String,
11+
}
12+
13+
#[derive(Debug, Clone, PartialEq)]
14+
pub struct AuditActionReport {
15+
pub report: AuditReport,
16+
}
17+
18+
pub fn audit_store(db_path: &Path, request: AuditActionRequest) -> ActionResult<AuditActionReport> {
19+
let report = if db_path.exists() {
20+
SQLiteMemoryStore::open_read_only(db_path)
21+
.and_then(|store| store.audit(&request.audit_type))
22+
.map_err(|err| err.to_string())?
23+
} else {
24+
audit_memories(&[], &request.audit_type).map_err(|err| err.to_string())?
25+
};
26+
Ok(AuditActionReport { report })
27+
}
28+
29+
#[cfg(test)]
30+
mod tests {
31+
use tempfile::tempdir;
32+
33+
use super::*;
34+
35+
#[test]
36+
fn audit_action_missing_store_reports_empty_audit_without_creating_store() {
37+
let dir = tempdir().unwrap();
38+
let db_path = dir.path().join(".tree-ring/memory.sqlite");
39+
40+
let report = audit_store(
41+
&db_path,
42+
AuditActionRequest {
43+
audit_type: "all".to_string(),
44+
},
45+
)
46+
.unwrap();
47+
48+
assert_eq!(report.report.memory_count, 0);
49+
assert!(!db_path.exists());
50+
}
51+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
4+
use serde_json::json;
5+
use tree_ring_memory_core::{decode_jsonl, normalize_import_events};
6+
use tree_ring_memory_sqlite::{ExportReport, ImportReport, SQLiteMemoryStore};
7+
8+
use super::ActionResult;
9+
10+
#[derive(Debug, Clone, PartialEq, Eq)]
11+
pub struct ExportActionRequest {
12+
pub output: Option<PathBuf>,
13+
pub include_sensitive: bool,
14+
pub include_superseded: bool,
15+
}
16+
17+
#[derive(Debug, Clone, PartialEq, Eq)]
18+
pub struct ExportActionReport {
19+
pub jsonl: Option<String>,
20+
pub output: Option<PathBuf>,
21+
pub report: ExportReport,
22+
}
23+
24+
#[derive(Debug, Clone, PartialEq, Eq)]
25+
pub struct ImportActionRequest {
26+
pub path: PathBuf,
27+
pub dry_run: bool,
28+
pub replace_existing: bool,
29+
}
30+
31+
#[derive(Debug, Clone, PartialEq, Eq)]
32+
pub struct ImportActionReport {
33+
pub path: PathBuf,
34+
pub report: ImportReport,
35+
}
36+
37+
pub fn export_jsonl(
38+
store: &SQLiteMemoryStore,
39+
request: ExportActionRequest,
40+
) -> ActionResult<ExportActionReport> {
41+
let (jsonl, report) = store
42+
.export_jsonl(request.include_sensitive, request.include_superseded)
43+
.map_err(|err| err.to_string())?;
44+
if let Some(output) = request.output {
45+
if let Some(parent) = output.parent() {
46+
if !parent.as_os_str().is_empty() {
47+
fs::create_dir_all(parent).map_err(|err| err.to_string())?;
48+
}
49+
}
50+
fs::write(&output, jsonl).map_err(|err| err.to_string())?;
51+
Ok(ExportActionReport {
52+
jsonl: None,
53+
output: Some(output),
54+
report,
55+
})
56+
} else {
57+
Ok(ExportActionReport {
58+
jsonl: Some(jsonl),
59+
output: None,
60+
report,
61+
})
62+
}
63+
}
64+
65+
pub fn import_jsonl(
66+
store: Option<&mut SQLiteMemoryStore>,
67+
request: ImportActionRequest,
68+
) -> ActionResult<ImportActionReport> {
69+
let input = fs::read_to_string(&request.path).map_err(|err| err.to_string())?;
70+
let report = if request.dry_run {
71+
let decoded = decode_jsonl(&input).map_err(|err| err.to_string())?;
72+
let events = normalize_import_events(decoded.events).map_err(|err| err.to_string())?;
73+
ImportReport {
74+
valid_count: events.len(),
75+
inserted_count: 0,
76+
replaced_count: 0,
77+
skipped_duplicate_count: 0,
78+
dry_run: true,
79+
}
80+
} else {
81+
let store = store.ok_or_else(|| {
82+
"import action requires an open writable store when dry_run=false".to_string()
83+
})?;
84+
store
85+
.import_jsonl(&input, false, request.replace_existing)
86+
.map_err(|err| err.to_string())?
87+
};
88+
Ok(ImportActionReport {
89+
path: request.path,
90+
report,
91+
})
92+
}
93+
94+
pub fn import_json_payload(report: &ImportActionReport) -> serde_json::Value {
95+
json!({
96+
"ok": true,
97+
"path": report.path,
98+
"valid_count": report.report.valid_count,
99+
"inserted_count": report.report.inserted_count,
100+
"replaced_count": report.report.replaced_count,
101+
"skipped_duplicate_count": report.report.skipped_duplicate_count,
102+
"dry_run": report.report.dry_run,
103+
})
104+
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use tempfile::tempdir;
109+
use tree_ring_memory_core::MemoryEvent;
110+
111+
use super::*;
112+
113+
#[test]
114+
fn export_action_can_return_stdout_jsonl() {
115+
let dir = tempdir().unwrap();
116+
let mut store = SQLiteMemoryStore::open(dir.path().join("memory.sqlite")).unwrap();
117+
store
118+
.put(&MemoryEvent::new("Export through shared action.", "lesson").unwrap())
119+
.unwrap();
120+
121+
let report = export_jsonl(
122+
&store,
123+
ExportActionRequest {
124+
output: None,
125+
include_sensitive: false,
126+
include_superseded: false,
127+
},
128+
)
129+
.unwrap();
130+
131+
assert_eq!(report.report.memory_count, 1);
132+
assert!(report.jsonl.unwrap().contains("memory_event"));
133+
}
134+
135+
#[test]
136+
fn import_action_dry_run_validates_without_writing() {
137+
let dir = tempdir().unwrap();
138+
let mut source = SQLiteMemoryStore::open(dir.path().join("source.sqlite")).unwrap();
139+
let target = SQLiteMemoryStore::open(dir.path().join("target.sqlite")).unwrap();
140+
source
141+
.put(&MemoryEvent::new("Import through shared action.", "lesson").unwrap())
142+
.unwrap();
143+
let (jsonl, _) = source.export_jsonl(false, false).unwrap();
144+
let input = dir.path().join("input.jsonl");
145+
fs::write(&input, jsonl).unwrap();
146+
147+
let report = import_jsonl(
148+
None,
149+
ImportActionRequest {
150+
path: input,
151+
dry_run: true,
152+
replace_existing: false,
153+
},
154+
)
155+
.unwrap();
156+
157+
assert_eq!(report.report.valid_count, 1);
158+
assert_eq!(target.list_all(true).unwrap().len(), 0);
159+
}
160+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::path::PathBuf;
2+
3+
use crate::integrations::{scan_integrations, IntegrationScanReport};
4+
5+
#[derive(Debug, Clone, PartialEq, Eq)]
6+
pub struct IntegrationScanRequest {
7+
pub source_root: PathBuf,
8+
}
9+
10+
#[derive(Debug, Clone, PartialEq)]
11+
pub struct IntegrationScanActionReport {
12+
pub report: IntegrationScanReport,
13+
}
14+
15+
pub fn scan(request: IntegrationScanRequest) -> IntegrationScanActionReport {
16+
IntegrationScanActionReport {
17+
report: scan_integrations(&request.source_root),
18+
}
19+
}
20+
21+
#[cfg(test)]
22+
mod tests {
23+
use std::fs;
24+
use tempfile::tempdir;
25+
26+
use super::*;
27+
28+
#[test]
29+
fn integration_action_scans_project_markers() {
30+
let dir = tempdir().unwrap();
31+
fs::write(dir.path().join("AGENTS.md"), "# Rules").unwrap();
32+
33+
let report = scan(IntegrationScanRequest {
34+
source_root: dir.path().to_path_buf(),
35+
});
36+
37+
assert!(report.report.detected_count > 0);
38+
}
39+
}

0 commit comments

Comments
 (0)