agentstatedeveloper_cli/commands/
hydrate.rs1use std::path::PathBuf;
14
15use anyhow::Result;
16use clap::Args;
17use serde_json::json;
18
19use agentstatedeveloper_core::{Engine, LedgerEntry, LedgerKind, hydrate_from_dir_with_cache};
20
21use crate::config::Config;
22
23#[derive(Debug, Args)]
24pub struct HydrateArgs {
25 #[arg(long)]
30 pub dir: Option<PathBuf>,
31
32 #[arg(long)]
35 pub verify: bool,
36}
37
38pub fn run(cfg: &Config, args: HydrateArgs) -> Result<()> {
39 let engine = Engine::open_sqlite(&cfg.db_path)?;
40 let dir = resolve_dir(args.dir)?;
41
42 if let Some(w) = agentstatedeveloper_core::outdated_hooks(&dir) {
46 eprintln!("warning: {w}");
47 }
48 let summary = hydrate_from_dir_with_cache(
49 &engine.repo,
50 engine.fts.as_ref(),
51 &engine.ref_name,
52 &dir,
53 &cfg.agent_id,
54 )?;
55
56 let (caches_synced, cache_sync_warning) = match engine.warm_caches() {
62 Ok(w) => (!w.skipped, None),
63 Err(e) => (false, Some(e.to_string())),
64 };
65
66 let mut out = json!({
67 "db": cfg.db_path.display().to_string(),
68 "dir": dir.join(".asd/v1").display().to_string(),
69 "symbols_loaded": summary.symbols_loaded,
70 "symbols_skipped": summary.symbols_skipped,
71 "effects_loaded": summary.effects_loaded,
72 "ledger_entries_loaded": summary.ledger_entries_loaded,
73 "ledger_entries_skipped": summary.ledger_entries_skipped,
74 "rebinds_replayed": summary.rebinds_replayed,
75 "blobs_rejected": summary.blobs_rejected,
76 "refs_dropped": summary.refs_dropped,
77 "missing_schema_version": summary.missing_schema_version,
78 "caches_synced": caches_synced,
79 "note": "commit history not restored; run `asd index` to rebuild the semantic index and call graph",
80 });
81 if let Some(w) = cache_sync_warning {
82 if let Some(obj) = out.as_object_mut() {
83 obj.insert("cache_sync_warning".to_string(), json!(w));
84 }
85 }
86
87 if args.verify {
88 let result = verify_hydration(&engine, &summary);
89 let ok = result.ok;
90 if let Some(obj) = out.as_object_mut() {
91 obj.insert("verify".to_string(), serde_json::to_value(&result)?);
92 }
93 println!("{}", serde_json::to_string_pretty(&out)?);
94 if !ok {
95 std::process::exit(1);
96 }
97 } else {
98 println!("{}", serde_json::to_string_pretty(&out)?);
99 }
100
101 Ok(())
102}
103
104#[derive(serde::Serialize)]
105struct VerifyResult {
106 ok: bool,
107 symbols_expected: usize,
108 symbols_actual: usize,
109 effects_expected: usize,
110 effects_actual: usize,
111 ledger_entries_expected: usize,
112 ledger_entries_actual: usize,
113 invariants_actual: usize,
114 discrepancies: Vec<String>,
115}
116
117fn verify_hydration(
118 engine: &Engine,
119 summary: &agentstatedeveloper_core::HydrateSummary,
120) -> VerifyResult {
121 let ref_name = &engine.ref_name;
122 let mut discrepancies: Vec<String> = Vec::new();
123
124 let symbols_actual = match engine.repo.get_tree(ref_name, "/asd/v1/index/by-qname") {
126 Ok(serde_json::Value::Object(map)) => map.len(),
127 _ => 0,
128 };
129 let symbols_expected = summary.symbols_loaded + summary.symbols_skipped;
132 if symbols_actual < summary.symbols_loaded {
133 discrepancies.push(format!(
134 "symbols: loaded {} but only {} readable from index",
135 summary.symbols_loaded, symbols_actual
136 ));
137 }
138
139 let effects_actual = match engine.repo.get_tree(ref_name, "/asd/v1/effects") {
141 Ok(serde_json::Value::Object(map)) => map.len(),
142 _ => 0,
143 };
144 let effects_expected = summary.effects_loaded;
145 if effects_actual < effects_expected {
146 discrepancies.push(format!(
147 "effects: loaded {} but only {} readable",
148 effects_expected, effects_actual
149 ));
150 }
151
152 let (ledger_entries_actual, invariants_actual) = count_ledger_entries(engine);
154 let ledger_entries_expected = summary.ledger_entries_loaded;
155 if ledger_entries_actual < ledger_entries_expected {
156 discrepancies.push(format!(
157 "ledger: loaded {} but only {} readable",
158 ledger_entries_expected, ledger_entries_actual
159 ));
160 }
161
162 if ledger_entries_expected > 0 && invariants_actual == 0 {
164 discrepancies.push(
165 "invariants: ledger entries were loaded but no invariant-kind entries found — possible kind mismatch".to_string(),
166 );
167 }
168
169 if summary.blobs_rejected > 0 {
171 discrepancies.push(format!(
172 "{} sidecar file(s) were rejected due to parse errors",
173 summary.blobs_rejected
174 ));
175 }
176
177 VerifyResult {
178 ok: discrepancies.is_empty(),
179 symbols_expected,
180 symbols_actual,
181 effects_expected,
182 effects_actual,
183 ledger_entries_expected,
184 ledger_entries_actual,
185 invariants_actual,
186 discrepancies,
187 }
188}
189
190fn count_ledger_entries(engine: &Engine) -> (usize, usize) {
191 let tree = match engine.repo.get_tree(&engine.ref_name, "/asd/v1/ledger") {
192 Ok(v) => v,
193 _ => return (0, 0),
194 };
195 let mut total = 0usize;
196 let mut invariants = 0usize;
197 if let Some(sym_map) = tree.as_object() {
198 for per_symbol in sym_map.values() {
199 if let Some(entry_map) = per_symbol.as_object() {
200 for entry_val in entry_map.values() {
201 if let Ok(e) = serde_json::from_value::<LedgerEntry>(entry_val.clone()) {
202 total += 1;
203 if e.kind == LedgerKind::Invariant {
204 invariants += 1;
205 }
206 }
207 }
208 }
209 }
210 }
211 (total, invariants)
212}
213
214fn resolve_dir(explicit: Option<PathBuf>) -> Result<PathBuf> {
215 match explicit {
216 Some(p) => Ok(p),
217 None => Ok(std::env::current_dir()?),
218 }
219}