1use serde::Serialize;
17use serde_json::{Value, json};
18
19pub const OWNER: &str = "asd";
22
23pub fn version() -> &'static str {
26 env!("CARGO_PKG_VERSION")
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct HelpParam {
31 pub name: &'static str,
32 pub required: bool,
33 pub desc: &'static str,
34}
35
36#[derive(Debug, Clone, Serialize)]
39pub struct HelpDoc {
40 pub feature: &'static str,
41 pub group: &'static str,
42 pub synopsis: &'static str,
43 pub syntax: &'static str,
44 pub params: &'static [HelpParam],
45 pub examples: &'static [&'static str],
46 pub gotchas: &'static [&'static str],
47 pub related: &'static [&'static str],
48}
49
50macro_rules! p {
51 ($name:literal, $req:literal, $desc:literal) => {
52 HelpParam {
53 name: $name,
54 required: $req,
55 desc: $desc,
56 }
57 };
58}
59
60pub const REGISTRY: &[HelpDoc] = &[
63 HelpDoc {
65 feature: "architecture",
66 group: "orient",
67 synopsis: "One-call cold-start overview: languages, packages, layers, call-graph communities, routes, hotspots.",
68 syntax: "asd architecture [--top <n=12>] [--agent]",
69 params: &[
70 p!(
71 "--top",
72 false,
73 "How many packages/hotspots to list (default 12)."
74 ),
75 p!("--agent", false, "Machine-readable JSON output."),
76 ],
77 examples: &["asd architecture", "asd architecture --top 20 --agent"],
78 gotchas: &["Read-only. Run this first in an unfamiliar repo, before touching anything."],
79 related: &["map", "status", "investigate"],
80 },
81 HelpDoc {
82 feature: "map",
83 group: "orient",
84 synopsis: "Initial-read project map; persists package boundaries and test-file roles as Ownership ledger entries.",
85 syntax: "asd map [--agent]",
86 params: &[p!("--agent", false, "Machine-readable JSON output.")],
87 examples: &["asd map"],
88 gotchas: &[
89 "Writes Ownership ledger entries — this is a read/orient step that also records structure.",
90 ],
91 related: &["architecture", "status"],
92 },
93 HelpDoc {
94 feature: "status",
95 group: "orient",
96 synopsis: "Workspace index health: trust score, dirty files, concept gaps, staleness.",
97 syntax: "asd status [--agent]",
98 params: &[p!("--agent", false, "Machine-readable JSON output.")],
99 examples: &["asd status"],
100 gotchas: &[
101 "For a single go/no-go reliability number use `trust`; to refresh a stale index use `index`/`reindex`.",
102 ],
103 related: &["trust", "index"],
104 },
105 HelpDoc {
106 feature: "trust",
107 group: "orient",
108 synopsis: "State Trust Score rollup — 'can I rely on asd right now?' as one number.",
109 syntax: "asd trust [--agent]",
110 params: &[p!("--agent", false, "Machine-readable JSON output.")],
111 examples: &["asd trust"],
112 gotchas: &["A low score means reindex or investigate before trusting asd's answers."],
113 related: &["status", "scorecard"],
114 },
115 HelpDoc {
117 feature: "prepare_change",
118 group: "understand",
119 synopsis: "One-call agent-ready context package for a planned change (investigate + impact + checklist).",
120 syntax: "asd prepare-change <query> [--agent]",
121 params: &[p!(
122 "query",
123 true,
124 "Free-text description of the change you intend to make."
125 )],
126 examples: &["asd prepare-change \"add rate limiting to the login endpoint\""],
127 gotchas: &[
128 "Call this FIRST before any non-trivial change — it bundles the understand+impact+checklist steps.",
129 ],
130 related: &["investigate", "impact", "checklist"],
131 },
132 HelpDoc {
133 feature: "investigate",
134 group: "understand",
135 synopsis: "Feature archaeology in one pass: entry points + call chains, effects, invariants, hazards.",
136 syntax: "asd investigate <query> [--agent]",
137 params: &[p!(
138 "query",
139 true,
140 "Feature/behavior to trace through the codebase."
141 )],
142 examples: &["asd investigate \"how does session token refresh work\""],
143 gotchas: &[
144 "Understand before touching. For blast radius of ONE known symbol use `impact` instead.",
145 ],
146 related: &["prepare_change", "impact", "context_for"],
147 },
148 HelpDoc {
149 feature: "impact",
150 group: "understand",
151 synopsis: "Blast-radius for ONE known symbol before editing: transitive callers, effects, invariants, tests, git touches.",
152 syntax: "asd impact <symbol> [--agent]",
153 params: &[p!(
154 "symbol",
155 true,
156 "Fully-qualified symbol name to analyze."
157 )],
158 examples: &["asd impact myapp::auth::verify_token"],
159 gotchas: &["Needs a known symbol. To find the symbol first use `search`/`references`."],
160 related: &["investigate", "since", "references"],
161 },
162 HelpDoc {
163 feature: "checklist",
164 group: "understand",
165 synopsis: "Pre-edit checklist for a free-text query: files, invariants, tests, hazards, effects.",
166 syntax: "asd checklist <query> [--agent]",
167 params: &[p!("query", true, "The change/area you're about to edit.")],
168 examples: &["asd checklist \"refactor the payment retry loop\""],
169 gotchas: &["Lighter than `prepare_change` — no full investigate/impact bundle."],
170 related: &["prepare_change", "impact"],
171 },
172 HelpDoc {
173 feature: "context_for",
174 group: "understand",
175 synopsis: "Deep per-symbol context for comma-separated qnames: signature, callers/callees, effects, ledger, tests.",
176 syntax: "asd context-for <qnames> [--agent]",
177 params: &[p!(
178 "qnames",
179 true,
180 "Comma-separated fully-qualified symbol names."
181 )],
182 examples: &["asd context-for myapp::db::pool,myapp::db::migrate"],
183 gotchas: &["For a whole-feature trace rather than named symbols, use `investigate`."],
184 related: &["impact", "investigate", "references"],
185 },
186 HelpDoc {
187 feature: "since",
188 group: "understand",
189 synopsis: "Symbols changed since a commit + combined blast radius (PR review).",
190 syntax: "asd since <ref> [--agent]",
191 params: &[p!(
192 "ref",
193 true,
194 "Git ref/commit to diff from (e.g. main, a SHA)."
195 )],
196 examples: &["asd since main"],
197 gotchas: &["Pairs with `impact` — this is the multi-symbol version for a whole diff."],
198 related: &["impact", "task_close"],
199 },
200 HelpDoc {
202 feature: "search",
203 group: "search",
204 synopsis: "Full ranked symbol search with confidence, uncertainty, and feedback adjustments (agent-grade).",
205 syntax: "asd search <query> [--agent]",
206 params: &[p!("query", true, "Concept or symbol to search for.")],
207 examples: &["asd search \"rate limiter\""],
208 gotchas: &[
209 "Heavier/ranked. For a lighter BM25 concept scan use `code_search`; for exact identifier hits use `references`.",
210 ],
211 related: &["code_search", "references", "context_for"],
212 },
213 HelpDoc {
214 feature: "code_search",
215 group: "search",
216 synopsis: "Ranked concept search (FTS5/BM25) — lighter than `search`.",
217 syntax: "asd search <query> [--agent] (CLI alias: code_search; distinct MCP tool)",
218 params: &[p!("query", true, "Concept text to rank against the index.")],
219 examples: &["asd search \"retry backoff\""],
220 gotchas: &[
221 "At the CLI this is the `search` command (code_search/code_query are aliases); as an MCP tool it is distinct from `search`.",
222 ],
223 related: &["search", "references"],
224 },
225 HelpDoc {
226 feature: "references",
227 group: "search",
228 synopsis: "Exact-identifier occurrences across the repo (rg-style completeness).",
229 syntax: "asd references <identifier> [--agent]",
230 params: &[p!(
231 "identifier",
232 true,
233 "Exact identifier to find every occurrence of."
234 )],
235 examples: &["asd references verify_token"],
236 gotchas: &[
237 "Exact-match, not ranked concept search — use `search`/`code_search` for fuzzy concepts.",
238 ],
239 related: &["search", "impact"],
240 },
241 HelpDoc {
243 feature: "task_close",
244 group: "ledger",
245 synopsis: "Write Proof/ValidationScenario ledger entries for the symbols affected by HEAD.",
246 syntax: "asd task-close [--agent]",
247 params: &[],
248 examples: &["asd task-close"],
249 gotchas: &[
250 "Run after making a change to record proof; pairs with `since` to know what HEAD touched.",
251 ],
252 related: &["since", "scorecard"],
253 },
254 HelpDoc {
256 feature: "help",
257 group: "meta",
258 synopsis: "Get exact syntax, examples, and gotchas for a feature on demand — call before using one you're unsure of.",
259 syntax: "asd help [topic]",
260 params: &[p!(
261 "topic",
262 false,
263 "Feature name or a phrase; omit to list the whole catalog."
264 )],
265 examples: &["asd help", "asd help impact", "asd help \"blast radius\""],
266 gotchas: &[
267 "Docs are version-pinned to the running binary, so they can't drift from the code.",
268 ],
269 related: &["architecture", "status"],
270 },
271 HelpDoc {
273 feature: "code_query",
274 group: "code",
275 synopsis: "Look up indexed symbols by exact structured filters (name_contains, kind, language), AND-combined, no ranking.",
276 syntax: "asd search <query> [--kind <k>] [--language <l>] [--limit <n=50>] (MCP tool: code_query)",
277 params: &[
278 p!("name_contains", false, "Substring match on qualified name."),
279 p!(
280 "kind",
281 false,
282 "Symbol kind: module, function, method, class, variable."
283 ),
284 p!("language", false, "Language filter (e.g. \"python\")."),
285 p!("limit", false, "Max results (default 50)."),
286 ],
287 examples: &["asd search \"pool\""],
288 gotchas: &[
289 "No relevance ranking — for concept discovery use `code_search`; for exact identifiers use `references`.",
290 ],
291 related: &["code_search", "references", "search"],
292 },
293 HelpDoc {
294 feature: "code_read",
295 group: "code",
296 synopsis: "Read one symbol by qname: returns its signature, effects, and ledger decisions.",
297 syntax: "asd read <qname>",
298 params: &[p!("qname", true, "Fully-qualified symbol name.")],
299 examples: &["asd read myapp::auth::verify_token"],
300 gotchas: &[
301 "For DEEP context (callers/callees, covering tests, token-budgeted) use `context_for`.",
302 ],
303 related: &["context_for", "effects", "ledger_get"],
304 },
305 HelpDoc {
306 feature: "callers",
307 group: "code",
308 synopsis: "List symbols that call the given symbol (inbound call edges, intra-module).",
309 syntax: "asd callers <qname>",
310 params: &[p!("qname", true, "Fully-qualified symbol name.")],
311 examples: &["asd callers myapp::auth::verify_token"],
312 gotchas: &[
313 "Intra-module edges only; for full blast radius with effects+tests use `impact`.",
314 ],
315 related: &["callees", "impact", "references"],
316 },
317 HelpDoc {
318 feature: "callees",
319 group: "code",
320 synopsis: "List symbols called by the given symbol (outbound call edges, intra-module).",
321 syntax: "asd callees <qname>",
322 params: &[p!("qname", true, "Fully-qualified symbol name.")],
323 examples: &["asd callees myapp::auth::login"],
324 gotchas: &["Intra-module edges only; misses dynamic dispatch and cross-language calls."],
325 related: &["callers", "impact"],
326 },
327 HelpDoc {
328 feature: "dead_code",
329 group: "code",
330 synopsis: "Candidate dead code: functions/methods with no inbound call edges. NOT definitive.",
331 syntax: "asd dead-code [--limit <n=50>] [--include-tests] [--agent]",
332 params: &[
333 p!("limit", false, "Max candidates to list (default 50)."),
334 p!(
335 "include_tests",
336 false,
337 "Include test functions (excluded by default)."
338 ),
339 ],
340 examples: &["asd dead-code"],
341 gotchas: &[
342 "Static graph misses reflection, framework callbacks, cross-repo API — confirm with `references` before deleting.",
343 ],
344 related: &["references", "callers", "endpoints"],
345 },
346 HelpDoc {
347 feature: "endpoints",
348 group: "code",
349 synopsis: "List cross-service endpoints (HTTP routes/clients, pub-sub) plus in-repo matched client↔route edges.",
350 syntax: "asd endpoints [--export] [--agent]",
351 params: &[],
352 examples: &["asd endpoints"],
353 gotchas: &[
354 "An unmatched client edge is a candidate contract-drift signal; cross-repo matching is Team-tier.",
355 ],
356 related: &["dead_code", "architecture"],
357 },
358 HelpDoc {
360 feature: "effects",
361 group: "effects",
362 synopsis: "Declared + transitively-inherited side effects for a symbol (I/O, network, fs, logging, mutation).",
363 syntax: "effects {qname} (MCP tool — no CLI verb)",
364 params: &[p!("qname", true, "Fully-qualified symbol name.")],
365 examples: &["effects {qname: \"myapp::db::pool\"}"],
366 gotchas: &[
367 "To check declared vs actual use `verify_effects`; to change them use `effect_declare`.",
368 ],
369 related: &["verify_effects", "effect_declare", "code_read"],
370 },
371 HelpDoc {
372 feature: "effect_declare",
373 group: "effects",
374 synopsis: "Overwrite the declared effects list for a symbol; routes through the policy gate.",
375 syntax: "effect_declare {qname, declared} (MCP tool — no CLI verb)",
376 params: &[
377 p!("qname", true, "Fully-qualified symbol name."),
378 p!(
379 "declared",
380 true,
381 "List of Effect objects: { effect, qualifiers, note }."
382 ),
383 p!(
384 "author_id",
385 false,
386 "Author id surfaced to the policy gate (default asd-mcp)."
387 ),
388 ],
389 examples: &[
390 "effect_declare {qname: \"myapp::io::write\", declared: [{effect: \"filesystem\"}]}",
391 ],
392 gotchas: &[
393 "Broadening (new effect categories) uses the asd.effect.declare.broadens policy action — may be denied or flagged.",
394 ],
395 related: &["effects", "verify_effects"],
396 },
397 HelpDoc {
398 feature: "verify_effects",
399 group: "effects",
400 synopsis: "Compare declared effects against what the static checker infers from source: ok/mismatch/unverified.",
401 syntax: "asd verify-effects <qname> [--write]",
402 params: &[
403 p!("qname", true, "Fully-qualified symbol name."),
404 p!(
405 "write",
406 false,
407 "Persist verification result to the store (default false)."
408 ),
409 ],
410 examples: &["asd verify-effects myapp::io::write"],
411 gotchas: &[
412 "unverified = source unreadable or no adapter for that language. Run before trusting `impact`'s effect set.",
413 ],
414 related: &["effects", "effect_declare", "impact"],
415 },
416 HelpDoc {
417 feature: "traces",
418 group: "effects",
419 synopsis: "Return execution trace records stored for a symbol (written by `asd trace`), newest-first.",
420 syntax: "traces {qname} (MCP tool — read side; write via `asd trace`)",
421 params: &[
422 p!("qname", true, "Fully-qualified symbol name."),
423 p!("limit", false, "Max trace records (default 20)."),
424 ],
425 examples: &["traces {qname: \"myapp::pipeline::run\"}"],
426 gotchas: &[
427 "Traces are produced by `asd trace` (Python only, sys.settrace); no CLI read verb.",
428 ],
429 related: &["effects", "verify_effects"],
430 },
431 HelpDoc {
433 feature: "ledger_get",
434 group: "ledger",
435 synopsis: "List ledger entries for a symbol, newest first; superseded entries omitted by default.",
436 syntax: "ledger_get {qname} (MCP tool — CLI reads go through `asd read` / `asd list`)",
437 params: &[
438 p!("qname", true, "Fully-qualified symbol name."),
439 p!(
440 "include_superseded",
441 false,
442 "Include superseded entries (default false)."
443 ),
444 ],
445 examples: &["ledger_get {qname: \"myapp::auth::verify_token\"}"],
446 gotchas: &[
447 "There is no ledger-get CLI verb — the CLI reads a symbol's ledger via `asd read <qname>`.",
448 ],
449 related: &["ledger_find", "ledger_append", "code_read"],
450 },
451 HelpDoc {
452 feature: "ledger_find",
453 group: "ledger",
454 synopsis: "Search ledger entries across ALL symbols by kind/tag/author_id, without knowing the anchoring symbol.",
455 syntax: "ledger_find {kind?, tag?, author_id?} (MCP tool — no CLI verb)",
456 params: &[
457 p!(
458 "kind",
459 false,
460 "Ledger kind (decision, assumption, constraint, rationale, hazard, tradeoff)."
461 ),
462 p!("tag", false, "Tag that must be present on the entry."),
463 p!("author_id", false, "Filter by author id."),
464 p!("limit", false, "Max results (default 50)."),
465 ],
466 examples: &["ledger_find {kind: \"hazard\"}"],
467 gotchas: &[
468 "To read ONE symbol's ledger use `ledger_get`; CLI reads go through `asd list`.",
469 ],
470 related: &["ledger_get", "ledger_append"],
471 },
472 HelpDoc {
473 feature: "ledger_append",
474 group: "ledger",
475 synopsis: "Append a ledger entry to a symbol; routes through the policy gate (may deny/allow/await-approval).",
476 syntax: "asd ledger append <qname> --kind <k> --summary <s> [--body ...] [--tags ...]",
477 params: &[
478 p!("qname", true, "Symbol the entry attaches to."),
479 p!(
480 "kind",
481 true,
482 "Ledger entry kind (decision, hazard, rationale, ...)."
483 ),
484 p!("summary", true, "One-line summary."),
485 p!("body", false, "Free-form body (markdown ok)."),
486 p!("tags", false, "Optional tags."),
487 ],
488 examples: &[
489 "asd ledger append myapp::db::pool --kind decision --summary \"pin pool size to 8\"",
490 ],
491 gotchas: &[
492 "Returns { entry_id, matched_policy, status }; status may be awaiting-approval — see `ledger_approve`.",
493 ],
494 related: &["ledger_supersede", "ledger_approve", "ledger_get"],
495 },
496 HelpDoc {
497 feature: "ledger_approve",
498 group: "ledger",
499 synopsis: "Approve a ledger entry tagged `awaiting-approval`; records approved-by/approved-at.",
500 syntax: "asd ledger approve <entry_id> --approver <id>",
501 params: &[
502 p!("entry_id", true, "Entry id from a prior ledger_append."),
503 p!(
504 "approver",
505 true,
506 "Approver identifier (recorded as approved-by:<id>)."
507 ),
508 p!(
509 "approver_kind",
510 false,
511 "Must match an approver:* tag on the entry (default human)."
512 ),
513 p!(
514 "message",
515 false,
516 "Optional approver rationale appended to the body."
517 ),
518 ],
519 examples: &["asd ledger approve led-abc123 --approver alice"],
520 gotchas: &["Approver kind/id must match one of the entry's original approver:* tags."],
521 related: &["ledger_reject", "ledger_withdraw", "ledger_append"],
522 },
523 HelpDoc {
524 feature: "ledger_reject",
525 group: "ledger",
526 synopsis: "Reject an awaiting-approval entry; records rejected-by/rejected-at with a required reason.",
527 syntax: "asd ledger reject <entry_id> --reviewer <id> --reason <text>",
528 params: &[
529 p!("entry_id", true, "Entry id to reject."),
530 p!(
531 "reviewer",
532 true,
533 "Reviewer id (recorded as rejected-by:<id>)."
534 ),
535 p!(
536 "reviewer_kind",
537 false,
538 "Same approver-match rule as approve (default human)."
539 ),
540 p!(
541 "reason",
542 true,
543 "Rejection reason (required); appended to the entry body."
544 ),
545 ],
546 examples: &[
547 "asd ledger reject led-abc123 --reviewer bob --reason \"superseded by new design\"",
548 ],
549 gotchas: &[
550 "Same approver-match rule as `ledger_approve` — reviewer must match an approver:* tag.",
551 ],
552 related: &["ledger_approve", "ledger_withdraw"],
553 },
554 HelpDoc {
555 feature: "ledger_withdraw",
556 group: "ledger",
557 synopsis: "Withdraw an awaiting-approval entry; must be called by the original author.",
558 syntax: "asd ledger withdraw <entry_id> --author-id <id>",
559 params: &[
560 p!("entry_id", true, "Entry id to withdraw."),
561 p!(
562 "author_id",
563 true,
564 "Must match the original entry.author.id."
565 ),
566 ],
567 examples: &["asd ledger withdraw led-abc123 --author-id asd-mcp"],
568 gotchas: &["Only the original author may withdraw; flips awaiting-approval → withdrawn."],
569 related: &["ledger_approve", "ledger_reject"],
570 },
571 HelpDoc {
572 feature: "ledger_supersede",
573 group: "ledger",
574 synopsis: "Append a new ledger entry that supersedes one or more existing entries for a symbol.",
575 syntax: "asd ledger supersede <qname> --supersede <ids> --kind <k> --summary <s>",
576 params: &[
577 p!("qname", true, "Symbol the new entry attaches to."),
578 p!("supersedes", true, "Entry ids superseded by the new entry."),
579 p!("kind", true, "Ledger kind for the new entry."),
580 p!("summary", true, "One-line summary."),
581 p!("body", false, "Optional body."),
582 ],
583 examples: &[
584 "asd ledger supersede myapp::db::pool --supersede led-old --kind decision --summary \"raise pool to 16\"",
585 ],
586 gotchas: &[
587 "Superseded entries are filtered out of default `ledger_get` results but remain retrievable.",
588 ],
589 related: &["ledger_append", "ledger_get"],
590 },
591 HelpDoc {
592 feature: "ledger_rebind",
593 group: "ledger",
594 synopsis: "Record a rename/move: move every ledger entry filed under the old symbol onto the new one, in one commit.",
595 syntax: "asd ledger rebind --from <symbol_id|qname> --to <qname> | --map <file.json>",
596 params: &[
597 p!(
598 "from_symbol_id",
599 true,
600 "symbol_id of the old symbol to re-parent from."
601 ),
602 p!(
603 "to_qname",
604 true,
605 "Qname of the new symbol to resolve and bind to."
606 ),
607 p!(
608 "agent_id",
609 false,
610 "Agent/user performing the rebind (default asd-mcp)."
611 ),
612 ],
613 examples: &[
614 "asd ledger rebind --from sym-old --to myapp::auth::verify",
615 "asd ledger rebind --map rebinds.json",
616 ],
617 gotchas: &[
618 "Run this whenever you rename a function/class so its ledger history isn't orphaned.",
619 "An orphaned symbol (`asd repair` lists them as `orphaned_ledger`) is no longer in the index: pass its symbol_id, not its old qname.",
620 "`--map` takes `{\"<symbol_id or qname>\": \"<new qname>\", …}`; every entry is checked before anything is written.",
621 ],
622 related: &["ledger_append", "ledger_get"],
623 },
624 HelpDoc {
625 feature: "annotate_commit",
626 group: "ledger",
627 synopsis: "Derive ledger annotations (decisions, invariants, hazards, proofs) from a git commit; dry-run by default.",
628 syntax: "asd annotate-commit [<sha=HEAD>] [--write] [--author <id>]",
629 params: &[
630 p!("sha", false, "Git commit SHA (default HEAD)."),
631 p!(
632 "write",
633 false,
634 "Actually persist entries (default false — dry-run)."
635 ),
636 p!("author", false, "Author id (defaults to git user.name)."),
637 ],
638 examples: &["asd annotate-commit", "asd annotate-commit HEAD --write"],
639 gotchas: &[
640 "Dry-run by default — pass --write to persist. Reads changed files + commit message to resolve symbols.",
641 ],
642 related: &["task_close", "ledger_append"],
643 },
644 HelpDoc {
646 feature: "invariant_add",
647 group: "invariant",
648 synopsis: "Record an invariant that must hold at a symbol (shortcut for ledger_append kind=invariant).",
649 syntax: "asd invariant add <qname> <summary>",
650 params: &[
651 p!("qname", true, "Fully-qualified symbol name."),
652 p!("summary", true, "One-line invariant summary."),
653 p!(
654 "author_id",
655 false,
656 "Author identifier (default asd-mcp-agent)."
657 ),
658 ],
659 examples: &["asd invariant add myapp::db::pool \"pool size must stay <= 16\""],
660 gotchas: &["Invariants surface in investigate, checklist, and prepare_change outputs."],
661 related: &["invariant_list", "ledger_append"],
662 },
663 HelpDoc {
664 feature: "invariant_list",
665 group: "invariant",
666 synopsis: "List invariants recorded against symbols; pass qname to filter to one symbol.",
667 syntax: "asd invariant list [<qname>]",
668 params: &[p!(
669 "qname",
670 false,
671 "Filter to one symbol; omit to list all."
672 )],
673 examples: &["asd invariant list"],
674 gotchas: &[
675 "Invariants are ledger entries of kind=invariant — also visible via `ledger_get`.",
676 ],
677 related: &["invariant_add", "ledger_get"],
678 },
679 HelpDoc {
681 feature: "feedback_mark",
682 group: "feedback",
683 synopsis: "Record a verdict on a search result (useful/noisy/missing/wrong_layer/already_covered/diagnostic_only).",
684 syntax: "asd feedback mark <query> <qname> <verdict>",
685 params: &[
686 p!("query", true, "The search query that produced this result."),
687 p!("qname", true, "Symbol being rated."),
688 p!(
689 "verdict",
690 true,
691 "useful | noisy | missing | wrong_layer | already_covered | diagnostic_only."
692 ),
693 p!(
694 "covered_by",
695 false,
696 "For already_covered: qname that covers this one (writes a Mapping entry)."
697 ),
698 p!(
699 "ttl_days",
700 false,
701 "Optional expiry in days after which the mark stops influencing ranking."
702 ),
703 ],
704 examples: &["asd feedback mark \"rate limiter\" myapp::mw::limit useful"],
705 gotchas: &[
706 "Verdicts persist as score adjustments applied to future searches.",
707 "Got one wrong? `asd feedback expire` lapses it so ranking stops seeing it.",
708 ],
709 related: &[
710 "feedback_list",
711 "feedback_expire",
712 "feedback_promote",
713 "search",
714 ],
715 },
716 HelpDoc {
717 feature: "feedback_expire",
718 group: "feedback",
719 synopsis: "Lapse a recorded verdict so it stops influencing search ranking.",
720 syntax: "asd feedback expire <entry-id> | --symbol <qname> | --query <text>",
721 params: &[
722 p!(
723 "entry_id",
724 false,
725 "Entry to expire, from `asd feedback list`. Or use --symbol / --query."
726 ),
727 p!(
728 "symbol",
729 false,
730 "Expire every verdict recorded against this qname."
731 ),
732 p!(
733 "query",
734 false,
735 "Expire every verdict for this query (matched against the stored lowercased form)."
736 ),
737 p!(
738 "dry_run",
739 false,
740 "Show what would be expired without writing."
741 ),
742 ],
743 examples: &[
744 "asd feedback list",
745 "asd feedback expire fb_58958221c119429798e16d6b070ed5d6",
746 "asd feedback expire --symbol myapp::mw::limit --dry-run",
747 ],
748 gotchas: &[
749 "Sets expires_at=now rather than deleting: the entry stays in `feedback list` marked expired, because it still explains why a past search ranked as it did.",
750 "One selector at a time — an entry id, --symbol or --query. Expiring everything because a flag was forgotten is not recoverable.",
751 "Re-expiring an already-lapsed entry is a no-op, not a timestamp rewrite.",
752 ],
753 related: &[
754 "feedback_mark",
755 "feedback_list",
756 "feedback_withdraw",
757 "search",
758 ],
759 },
760 HelpDoc {
761 feature: "feedback_withdraw",
762 group: "feedback",
763 synopsis: "Retract a verdict that should never have been recorded.",
764 syntax: "asd feedback withdraw <entry-id> [--by <who>] [--reason <text>]",
765 params: &[
766 p!(
767 "entry_id",
768 true,
769 "Entry to retract, from `asd feedback list`."
770 ),
771 p!("by", false, "Who is retracting it (default: asd-cli)."),
772 p!("reason", false, "Why. Stored with the withdrawal."),
773 ],
774 examples: &[
775 "asd feedback list",
776 "asd feedback withdraw fb_58958221c119429798e16d6b070ed5d6 --reason \"wrong symbol\"",
777 ],
778 gotchas: &[
779 "Different from `expire`: expire means the verdict was right but is no longer relevant; withdraw means it was wrong. Only withdraw records who retracted it and why.",
780 "A withdrawal cannot be undone by future-dating an expiry — the two states are independent.",
781 "The entry stays in `feedback list`, marked withdrawn, because it still explains why a past search ranked as it did.",
782 "Withdrawing twice keeps the original timestamp rather than moving it.",
783 ],
784 related: &[
785 "feedback_expire",
786 "feedback_mark",
787 "feedback_list",
788 "feedback_purge",
789 ],
790 },
791 HelpDoc {
792 feature: "feedback_purge",
793 group: "feedback",
794 synopsis: "Permanently delete a verdict from both stores. Escape hatch — prefer `withdraw`.",
795 syntax: "asd feedback purge <entry-id> --yes",
796 params: &[
797 p!("entry_id", true, "Entry to delete permanently."),
798 p!(
799 "yes",
800 true,
801 "Required. Without it the command explains and refuses."
802 ),
803 ],
804 examples: &["asd feedback purge fb_58958221c119429798e16d6b070ed5d6 --yes"],
805 gotchas: &[
806 "This REWRITES HISTORY in a store that is otherwise append-only. `asd feedback withdraw` is almost always the right verb: it retracts the verdict while keeping the record, which still explains why a past search ranked as it did.",
807 "Purge is for data that must not exist at all — test entries, or a secret pasted into a --note.",
808 "Removes from the ASG store first, then the SQLite search cache. If the cache step fails it errors loudly and names the fix (`asd index`), because a half-purge leaves `feedback list` still showing the entry.",
809 ],
810 related: &["feedback_withdraw", "feedback_expire", "feedback_list"],
811 },
812 HelpDoc {
813 feature: "feedback_promote",
814 group: "feedback",
815 synopsis: "Designate a symbol as canonical source-of-truth for a concept (writes Ownership entry, 3x ranking boost).",
816 syntax: "asd feedback promote-as-truth <qname> --concept <text>",
817 params: &[
818 p!("qname", true, "Symbol to promote."),
819 p!(
820 "concept",
821 true,
822 "The domain concept this symbol owns (e.g. \"login rate limiting\")."
823 ),
824 p!(
825 "author_id",
826 false,
827 "Author identifier (default asd-mcp-agent)."
828 ),
829 ],
830 examples: &[
831 "asd feedback promote-as-truth myapp::mw::limit --concept \"login rate limiting\"",
832 ],
833 gotchas: &["MCP tool is feedback_promote; the CLI verb is `feedback promote-as-truth`."],
834 related: &["feedback_mark", "feedback_list"],
835 },
836 HelpDoc {
837 feature: "feedback_list",
838 group: "feedback",
839 synopsis: "List recorded feedback verdicts; pass qname to filter to one symbol.",
840 syntax: "asd feedback list [<qname>]",
841 params: &[p!(
842 "qname",
843 false,
844 "Filter to one symbol; omit to list all."
845 )],
846 examples: &["asd feedback list"],
847 gotchas: &["Use to audit search-quality signals before trusting or overriding rankings."],
848 related: &["feedback_mark", "feedback_promote"],
849 },
850 HelpDoc {
852 feature: "scratch_write",
853 group: "scratch",
854 synopsis: "Write a local-only draft scratch note scoped to a symbol/workflow/session; no policy gate.",
855 syntax: "asd scratch write <content> [--symbol <qname>] [--workflow <name>] [--planning]",
856 params: &[
857 p!("content", true, "Working notes content (markdown ok)."),
858 p!("symbol", false, "Symbol to attach the note to."),
859 p!("workflow", false, "Named investigation context."),
860 p!(
861 "ttl_hours",
862 false,
863 "Time-to-live in hours; no expiry when unset."
864 ),
865 p!(
866 "planning",
867 false,
868 "Mark as pre-implementation notes; adds 'planning' tag, symbol need not exist yet."
869 ),
870 ],
871 examples: &[
872 "asd scratch write \"suspect cache is drift source\" --symbol myapp::cache::get",
873 ],
874 gotchas: &[
875 "Scratch is local-only and NOT synced by `asd sync`. Promote durable notes with `scratch_promote`.",
876 ],
877 related: &["scratch_list", "scratch_promote", "scratch_read"],
878 },
879 HelpDoc {
880 feature: "scratch_list",
881 group: "scratch",
882 synopsis: "List scratch entries (default: draft, non-expired); filter by symbol/workflow/session/status.",
883 syntax: "asd scratch list [--symbol <q>] [--workflow <w>] [--status <s>]",
884 params: &[
885 p!("symbol", false, "Filter by symbol qname."),
886 p!("workflow", false, "Filter by workflow name."),
887 p!("session", false, "Filter by session/agent_id."),
888 p!(
889 "status",
890 false,
891 "draft | promoted | discarded (default draft)."
892 ),
893 ],
894 examples: &["asd scratch list"],
895 gotchas: &["Pass status=null (omit filter) to see all statuses, not just drafts."],
896 related: &["scratch_write", "scratch_read", "scratch_clean"],
897 },
898 HelpDoc {
899 feature: "scratch_read",
900 group: "scratch",
901 synopsis: "Read a single scratch entry by scratch_id; returns the full ScratchEntry.",
902 syntax: "asd scratch read <scratch_id>",
903 params: &[p!("scratch_id", true, "Scratch entry id (scr_…).")],
904 examples: &["asd scratch read scr_abc123"],
905 gotchas: &["Get the id from `scratch_list`."],
906 related: &["scratch_list", "scratch_update"],
907 },
908 HelpDoc {
909 feature: "scratch_update",
910 group: "scratch",
911 synopsis: "Replace the content of an existing draft scratch entry entirely.",
912 syntax: "asd scratch update <scratch_id> <content>",
913 params: &[
914 p!("scratch_id", true, "Scratch entry id to update."),
915 p!(
916 "content",
917 true,
918 "Replacement content (replaces previous entirely)."
919 ),
920 ],
921 examples: &["asd scratch update scr_abc123 \"confirmed: cache TTL is the drift source\""],
922 gotchas: &["Only draft entries are updatable; content is replaced, not appended."],
923 related: &["scratch_write", "scratch_read"],
924 },
925 HelpDoc {
926 feature: "scratch_discard",
927 group: "scratch",
928 synopsis: "Mark a scratch entry as discarded (soft-delete); purge later with scratch_clean.",
929 syntax: "asd scratch discard <scratch_id>",
930 params: &[p!("scratch_id", true, "Scratch entry id to discard.")],
931 examples: &["asd scratch discard scr_abc123"],
932 gotchas: &["Soft-delete only — `scratch_clean` permanently purges discarded entries."],
933 related: &["scratch_clean", "scratch_list"],
934 },
935 HelpDoc {
936 feature: "scratch_promote",
937 group: "scratch",
938 synopsis: "Promote a draft scratch entry to a durable ledger entry (goes through policy + audit).",
939 syntax: "asd scratch promote <scratch_id> --kind <k>",
940 params: &[
941 p!("scratch_id", true, "Scratch entry id to promote."),
942 p!(
943 "kind",
944 true,
945 "Ledger kind (decision, hazard, invariant, proof, ...)."
946 ),
947 p!(
948 "qname",
949 false,
950 "Symbol qname; required if the scratch entry has no symbol attached."
951 ),
952 p!(
953 "summary",
954 false,
955 "One-line summary (defaults to first non-empty line of content)."
956 ),
957 ],
958 examples: &["asd scratch promote scr_abc123 --kind decision --symbol myapp::cache::get"],
959 gotchas: &[
960 "Requires a symbol via --qname or the entry's existing symbol_id; returns { promoted_to, entry_id }.",
961 ],
962 related: &["scratch_write", "ledger_append"],
963 },
964 HelpDoc {
965 feature: "scratch_clean",
966 group: "scratch",
967 synopsis: "Permanently delete scratch entries older than N hours matching a status filter; dry_run to preview.",
968 syntax: "asd scratch clean --older-than <dur> [--status <list>] [--dry-run]",
969 params: &[
970 p!(
971 "older_than_hours",
972 true,
973 "Delete entries older than this many hours."
974 ),
975 p!(
976 "statuses",
977 false,
978 "Comma-separated statuses to clean (default \"discarded,promoted\")."
979 ),
980 p!(
981 "dry_run",
982 false,
983 "Report what would be deleted without removing anything."
984 ),
985 ],
986 examples: &["asd scratch clean --older-than 7d --dry-run"],
987 gotchas: &[
988 "Permanent delete — always dry-run first. MCP param is older_than_hours; the CLI takes a duration like 7d/24h. Defaults to discarded+promoted, sparing drafts.",
989 ],
990 related: &["scratch_discard", "scratch_list"],
991 },
992 HelpDoc {
994 feature: "think_speculate",
995 group: "think",
996 synopsis: "Record a Hypothesis about a symbol — a hunch with a confidence in [0.0, 1.0].",
997 syntax: "asd think speculate <qname> --conf <f> --summary <text>",
998 params: &[
999 p!("qname", true, "Symbol the hypothesis is about."),
1000 p!(
1001 "confidence",
1002 true,
1003 "Confidence in [0.0, 1.0]; below 0.3 is hidden from auto-surface by default."
1004 ),
1005 p!("summary", true, "One-line hunch."),
1006 p!("body", false, "Optional detail."),
1007 ],
1008 examples: &[
1009 "asd think speculate myapp::cache::get --conf 0.6 --summary \"probably the drift source\"",
1010 ],
1011 gotchas: &[
1012 "Marks below 0.3 are excluded from prepare_change/context_for prior-thinking. Idempotent per (qname, summary).",
1013 ],
1014 related: &["think_model", "think_list", "think_question"],
1015 },
1016 HelpDoc {
1017 feature: "think_model",
1018 group: "think",
1019 synopsis: "Record a MentalModel — a multi-symbol structural understanding spanning several qnames.",
1020 syntax: "asd think model <name> --symbols <csv> --summary <text>",
1021 params: &[
1022 p!("name", true, "Model name."),
1023 p!(
1024 "symbols",
1025 true,
1026 "Comma-separated qnames the model spans (first is anchor)."
1027 ),
1028 p!("summary", true, "The structural understanding."),
1029 ],
1030 examples: &[
1031 "asd think model \"auth flow\" --symbols myapp::auth::login,myapp::auth::verify --summary \"login calls verify then issues token\"",
1032 ],
1033 gotchas: &[
1034 "Anchored on the FIRST symbol; body carries the full list. Idempotent by (name, summary).",
1035 ],
1036 related: &["think_speculate", "think_list"],
1037 },
1038 HelpDoc {
1039 feature: "think_failed",
1040 group: "think",
1041 synopsis: "Record a FailedAttempt — what you tried and why it didn't work, so the next session doesn't re-tread it.",
1042 syntax: "asd think failed <qname> --tried <text> --because <text>",
1043 params: &[
1044 p!("qname", true, "Symbol the attempt concerned."),
1045 p!("tried", true, "What you tried."),
1046 p!("because", true, "Why it didn't work."),
1047 ],
1048 examples: &[
1049 "asd think failed myapp::cache::get --tried \"raise TTL\" --because \"drift persisted\"",
1050 ],
1051 gotchas: &["Idempotent by (qname, tried)."],
1052 related: &["think_question", "think_list"],
1053 },
1054 HelpDoc {
1055 feature: "think_question",
1056 group: "think",
1057 synopsis: "Record an OpenQuestion — a known unknown blocking confident action.",
1058 syntax: "asd think question <qname> --q <text>",
1059 params: &[
1060 p!("qname", true, "Symbol the question concerns."),
1061 p!("question", true, "The known unknown."),
1062 ],
1063 examples: &["asd think question myapp::cache::get --q \"is eviction concurrent-safe?\""],
1064 gotchas: &[
1065 "Be generous — every recorded question is one the next session won't re-ask. Idempotent by (qname, question).",
1066 ],
1067 related: &["think_failed", "think_list"],
1068 },
1069 HelpDoc {
1070 feature: "think_list",
1071 group: "think",
1072 synopsis: "List captured thinking (Hypothesis/MentalModel/FailedAttempt/OpenQuestion); filter by kind or symbol.",
1073 syntax: "asd think list [--kind <k>] [--symbol <qname>]",
1074 params: &[
1075 p!(
1076 "kind",
1077 false,
1078 "hypothesis | mental_model | failed_attempt | open_question."
1079 ),
1080 p!("symbol", false, "Filter to one symbol."),
1081 ],
1082 examples: &["asd think list"],
1083 gotchas: &[
1084 "Call when resuming work on an area to see what prior sessions concluded or got stuck on.",
1085 ],
1086 related: &[
1087 "think_speculate",
1088 "think_model",
1089 "think_failed",
1090 "think_question",
1091 ],
1092 },
1093 HelpDoc {
1095 feature: "conclusions_list",
1096 group: "conclusions",
1097 synopsis: "List ledger entries bucketed by the six conclusion classes (decisions, classifications, mappings, hazards, recipes, followups).",
1098 syntax: "asd conclusions list [--class <c>] [--symbol <qname>]",
1099 params: &[
1100 p!(
1101 "class",
1102 false,
1103 "One of: decisions | classifications | mappings | hazards | recipes | followups."
1104 ),
1105 p!("symbol", false, "Filter to one symbol qname."),
1106 ],
1107 examples: &["asd conclusions list"],
1108 gotchas: &["Audit what conclusions exist before exporting to .asd/conclusions/*.jsonl."],
1109 related: &["conclusions_export", "conclusions_import"],
1110 },
1111 HelpDoc {
1112 feature: "conclusions_export",
1113 group: "conclusions",
1114 synopsis: "Write all ledger conclusions to compact JSONL files (one per class) under .asd/conclusions/.",
1115 syntax: "asd conclusions export [--out <dir>]",
1116 params: &[p!(
1117 "out",
1118 false,
1119 "Output dir (default .asd/conclusions/ beside the db)."
1120 )],
1121 examples: &["asd conclusions export"],
1122 gotchas: &["Byte-stable when no new entries — safe to run from a pre-commit hook."],
1123 related: &["conclusions_import", "conclusions_list", "sync"],
1124 },
1125 HelpDoc {
1126 feature: "conclusions_import",
1127 group: "conclusions",
1128 synopsis: "Read .asd/conclusions/*.jsonl back into the local ledger; idempotent (keyed by entry_id).",
1129 syntax: "asd conclusions import [--in <dir>]",
1130 params: &[p!(
1131 "in",
1132 false,
1133 "Input dir with *.jsonl (default .asd/conclusions/)."
1134 )],
1135 examples: &["asd conclusions import"],
1136 gotchas: &[
1137 "Run after `git pull` or on a fresh clone to populate ASG with committed conclusions.",
1138 ],
1139 related: &["conclusions_export", "conclusions_list"],
1140 },
1141 HelpDoc {
1143 feature: "recipe_classify_test_migration",
1144 group: "recipes",
1145 synopsis: "Classify test-tier symbols matching a query into migration actions (Delete/Gate/Run/KeepAsCovered/Review).",
1146 syntax: "asd recipe classify-test-migration <query> [--limit <n=50>]",
1147 params: &[
1148 p!(
1149 "query",
1150 true,
1151 "Search query — finds candidate test symbols."
1152 ),
1153 p!("limit", false, "Max candidates to classify (default 50)."),
1154 ],
1155 examples: &["asd recipe classify-test-migration \"song player tests\""],
1156 gotchas: &["For stale test FILES that may need moving, use `recipe_migrate_stale_tests`."],
1157 related: &["recipe_migrate_stale_tests", "test_summary"],
1158 },
1159 HelpDoc {
1160 feature: "recipe_migrate_stale_tests",
1161 group: "recipes",
1162 synopsis: "Build a migration plan for stale test files; adds a Move action when a Mapping entry carries move_to.",
1163 syntax: "asd recipe migrate-stale-tests <query> [--limit <n=50>]",
1164 params: &[
1165 p!("query", true, "Search query — finds candidate test files."),
1166 p!("limit", false, "Max candidates (default 50)."),
1167 ],
1168 examples: &["asd recipe migrate-stale-tests \"song player tests\""],
1169 gotchas: &[
1170 "Same output shape as classify-test-migration plus Move; falls back to the classify tree without a move_to.",
1171 ],
1172 related: &["recipe_classify_test_migration"],
1173 },
1174 HelpDoc {
1176 feature: "audit_tail",
1177 group: "audit",
1178 synopsis: "Read back audit events from the configured JSONL log; filter by event_type, actor, outcome, or since cursor.",
1179 syntax: "asd audit tail [--event-type <t>] [--actor <id>] [--outcome <o>] [--since <event_id>]",
1180 params: &[
1181 p!(
1182 "event_type",
1183 false,
1184 "Substring on event_type (e.g. \"ledger.\" for all ledger events)."
1185 ),
1186 p!(
1187 "since",
1188 false,
1189 "Return only events after this event_id (exclusive)."
1190 ),
1191 p!("actor", false, "Exact match on actor_id."),
1192 p!("outcome", false, "Exact match on outcome."),
1193 p!("limit", false, "Max events (default 200, max 1000)."),
1194 ],
1195 examples: &["asd audit tail --event-type ledger."],
1196 gotchas: &["Returns configured:false when ASD_AUDIT_LOG was not set at server startup."],
1197 related: &["audit_verify", "ledger_append"],
1198 },
1199 HelpDoc {
1200 feature: "audit_verify",
1201 group: "audit",
1202 synopsis: "Verify the hash-chain integrity of the configured audit log (Enterprise tier — requires asd-pro).",
1203 syntax: "asd audit verify",
1204 params: &[],
1205 examples: &["asd audit verify"],
1206 gotchas: &[
1207 "Commercial feature (Enterprise tier); needs asd-pro and a configured ASD_AUDIT_LOG.",
1208 ],
1209 related: &["audit_tail"],
1210 },
1211 HelpDoc {
1213 feature: "health",
1214 group: "quality",
1215 synopsis: "Liveness check for the MCP server: status, db path, indexed symbol count, and total artifact counts.",
1216 syntax: "health (MCP tool — no CLI verb)",
1217 params: &[],
1218 examples: &["health {}"],
1219 gotchas: &[
1220 "For index freshness/dirty files use `status`; for a go/no-go reliability score use `trust`.",
1221 ],
1222 related: &["status", "trust"],
1223 },
1224 HelpDoc {
1225 feature: "reindex",
1226 group: "quality",
1227 synopsis: "Re-parse a file or directory and refresh the symbol index, effects, and call graph.",
1228 syntax: "asd reindex <path>",
1229 params: &[p!(
1230 "path",
1231 true,
1232 "Absolute or relative path to a source file or directory."
1233 )],
1234 examples: &["asd reindex src/", "asd index src/"],
1235 gotchas: &[
1236 "Alias of `asd index`. After indexing, run `sync` so updated state travels with the next commit.",
1237 ],
1238 related: &["sync", "status"],
1239 },
1240 HelpDoc {
1241 feature: "sync",
1242 group: "quality",
1243 synopsis: "Flush live ASG state (symbols, effects, ledger, rebinds) into the .asd/v1/ sidecar for git commit.",
1244 syntax: "asd sync [--prune] [--dir <path>]",
1245 params: &[
1246 p!(
1247 "dir",
1248 false,
1249 "Project root to sync into (.asd/v1/ appended); defaults to the active db's dir."
1250 ),
1251 p!(
1252 "prune",
1253 false,
1254 "Also drop orphaned sidecar files for symbols that no longer exist."
1255 ),
1256 ],
1257 examples: &["asd sync", "asd sync --prune"],
1258 gotchas: &[
1259 "Run after `reindex` or edits so committed state stays current; the pre-commit hook runs sync --prune.",
1260 ],
1261 related: &["reindex", "conclusions_export"],
1262 },
1263 HelpDoc {
1264 feature: "test_summary",
1265 group: "quality",
1266 synopsis: "Summarize raw test-runner output into a compact failures-only report: { runner, passed, failed, failures[] }.",
1267 syntax: "cargo test 2>&1 | asd test-summary (reads output on stdin)",
1268 params: &[p!(
1269 "output",
1270 true,
1271 "Raw test-runner output (cargo/pytest parsed precisely; others via generic scan)."
1272 )],
1273 examples: &["cargo test 2>&1 | asd test-summary"],
1274 gotchas: &[
1275 "MCP passes output as a param; the CLI reads it on stdin. Cargo and pytest are parsed precisely.",
1276 ],
1277 related: &["recipe_classify_test_migration", "scorecard"],
1278 },
1279 HelpDoc {
1280 feature: "scopes_list",
1281 group: "quality",
1282 synopsis: "List named scope aliases from .asd/scopes.toml — narrow noisy searches with --scope / --paths.",
1283 syntax: "asd scopes list",
1284 params: &[],
1285 examples: &["asd scopes list"],
1286 gotchas: &[
1287 "Scopes feed the --scope/--paths flags on search, prepare_change, investigate, impact, checklist, since.",
1288 ],
1289 related: &["search", "investigate"],
1290 },
1291 HelpDoc {
1292 feature: "scorecard",
1293 group: "quality",
1294 synopsis: "Five-dimension benchmark scorecard: truth, feedback, change, uncertainty, workflow — each 0-100.",
1295 syntax: "asd scorecard [--drill-down <dim>] [--scope <s>] [--paths <glob>]",
1296 params: &[
1297 p!("scope", false, "Named scope alias."),
1298 p!("paths", false, "Comma-separated glob patterns."),
1299 p!(
1300 "drill_down",
1301 false,
1302 "truth | feedback | change | uncertainty | workflow — show gap symbols."
1303 ),
1304 p!("limit", false, "Max symbols in drill-down (default 10)."),
1305 ],
1306 examples: &["asd scorecard", "asd scorecard --drill-down truth"],
1307 gotchas: &["Use drill_down to see which symbols are dragging a dimension down."],
1308 related: &["trust", "status", "test_summary"],
1309 },
1310 HelpDoc {
1311 feature: "worktree",
1312 group: "workflow",
1313 synopsis: "Plan-scoped git worktrees: isolated files + HEAD per unit of work so parallel agents don't clobber each other.",
1314 syntax: "asd worktree start|list|finish <plan> [--from <ref>] [--shared-target] [--clone [--url <remote>]] [--push] [--keep]",
1315 params: &[
1316 p!(
1317 "plan",
1318 true,
1319 "Plan name; the worktree is ../<repo>-wt-<plan> on branch plan/<plan>."
1320 ),
1321 p!(
1322 "--shared-target",
1323 false,
1324 "start: share one Rust build cache (<repo>/.wt-target) instead of a per-worktree target/."
1325 ),
1326 p!(
1327 "--clone",
1328 false,
1329 "isolate via a fresh clone (own .git, ../<repo>-clone-<plan>) for remote agents; merge-back is via origin."
1330 ),
1331 p!(
1332 "--push",
1333 false,
1334 "finish: git push the branch (with --clone, pushes to origin before teardown)."
1335 ),
1336 ],
1337 examples: &[
1338 "asd worktree start add-oauth",
1339 "asd worktree list",
1340 "asd worktree finish add-oauth --push",
1341 "asd worktree start add-oauth --clone",
1342 ],
1343 gotchas: &[
1344 "The CLI can't cd you — `start` prints the path; open your session there.",
1345 "`finish` refuses on a dirty worktree/clone and never switches the main checkout's HEAD; it force-tears-down (removes target/ too).",
1346 "worktree = local isolation (shared .git); --clone = remote isolation (own .git, merge-back via origin).",
1347 ],
1348 related: &["status", "trust"],
1349 },
1350];
1351
1352const STRONG_MATCH: u32 = 200;
1356
1357fn est_tokens(doc: &HelpDoc) -> usize {
1358 let mut chars = doc.synopsis.len() + doc.syntax.len();
1359 for p in doc.params {
1360 chars += p.name.len() + p.desc.len();
1361 }
1362 for e in doc.examples {
1363 chars += e.len();
1364 }
1365 for g in doc.gotchas {
1366 chars += g.len();
1367 }
1368 for r in doc.related {
1369 chars += r.len();
1370 }
1371 chars / 4
1372}
1373
1374fn doc_json(doc: &HelpDoc) -> Value {
1375 json!({
1376 "feature": doc.feature,
1377 "owner": OWNER,
1378 "version": version(),
1379 "synopsis": doc.synopsis,
1380 "syntax": doc.syntax,
1381 "params": doc.params,
1382 "examples": doc.examples,
1383 "gotchas": doc.gotchas,
1384 "related": doc.related,
1385 "help_tokens": est_tokens(doc),
1386 })
1387}
1388
1389fn score(doc: &HelpDoc, q: &str) -> u32 {
1393 let feat = doc.feature.to_lowercase();
1394 if feat == q {
1395 return 1000;
1396 }
1397 let mut s = 0u32;
1398 if feat.contains(q) || q.contains(&feat) {
1399 s += STRONG_MATCH;
1400 }
1401 let syn = doc.synopsis.to_lowercase();
1402 for tok in q
1403 .split(|c: char| !c.is_alphanumeric())
1404 .filter(|t| t.len() > 2)
1405 {
1406 if feat.contains(tok) {
1407 s += 100;
1408 }
1409 if syn.contains(tok) {
1410 s += 20;
1411 }
1412 }
1413 s
1414}
1415
1416pub fn catalog() -> Value {
1418 use std::collections::BTreeMap;
1419 let mut groups: BTreeMap<&str, Vec<Value>> = BTreeMap::new();
1420 for doc in REGISTRY {
1421 groups.entry(doc.group).or_default().push(json!({
1422 "feature": doc.feature,
1423 "synopsis": doc.synopsis,
1424 }));
1425 }
1426 json!({
1427 "owner": OWNER,
1428 "version": version(),
1429 "usage": "Call `help <feature>` for exact syntax, examples, and gotchas before using a feature.",
1430 "groups": groups,
1431 "feature_count": REGISTRY.len(),
1432 })
1433}
1434
1435pub fn respond(topic: Option<&str>) -> Value {
1437 let q = topic.map(|t| t.trim().to_lowercase()).unwrap_or_default();
1438 if q.is_empty() {
1439 return catalog();
1440 }
1441 let mut ranked: Vec<(&HelpDoc, u32)> = REGISTRY
1442 .iter()
1443 .map(|d| (d, score(d, &q)))
1444 .filter(|(_, s)| *s > 0)
1445 .collect();
1446 ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.feature.cmp(b.0.feature)));
1447
1448 match ranked.first() {
1449 Some((doc, s)) if *s >= STRONG_MATCH => {
1450 let mut out = doc_json(doc);
1451 let also: Vec<&str> = ranked
1452 .iter()
1453 .skip(1)
1454 .take(4)
1455 .map(|(d, _)| d.feature)
1456 .collect();
1457 if let Some(obj) = out.as_object_mut() {
1458 obj.insert("also".into(), json!(also));
1459 }
1460 out
1461 }
1462 Some(_) => json!({
1463 "query": q,
1464 "owner": OWNER,
1465 "matches": ranked.iter().take(6).map(|(d, _)| json!({
1466 "feature": d.feature,
1467 "synopsis": d.synopsis,
1468 })).collect::<Vec<_>>(),
1469 "hint": "No exact feature match — `help <feature>` for one of these, or `help` for the full catalog.",
1470 }),
1471 None => json!({
1472 "not_found": q,
1473 "owner": OWNER,
1474 "did_you_mean": REGISTRY.iter().map(|d| d.feature).collect::<Vec<_>>(),
1475 "hint": "No asd feature matched. It may be a `ctx` (CTXone) feature — try `ctx help <topic>`.",
1476 }),
1477 }
1478}
1479
1480pub fn resolve(topic: Option<&str>, allow_proxy: bool) -> Value {
1488 let local = respond(topic);
1489 if !allow_proxy || local.get("not_found").is_none() {
1490 return local;
1491 }
1492 let Some(t) = topic.map(str::trim).filter(|t| !t.is_empty()) else {
1493 return local;
1494 };
1495 let Some(index) = read_index() else {
1496 return local;
1497 };
1498 let Some(tools) = index.get("tools").and_then(|v| v.as_object()) else {
1499 return local;
1500 };
1501 for tool in tools.keys().filter(|k| k.as_str() != OWNER) {
1503 let Some((bin, args)) = json_invocation(tool, t) else {
1504 continue;
1505 };
1506 let Ok(out) = std::process::Command::new(bin).args(&args).output() else {
1507 continue;
1508 };
1509 if !out.status.success() {
1510 continue;
1511 }
1512 if let Ok(mut v) = serde_json::from_slice::<Value>(&out.stdout)
1513 && v.get("feature").is_some()
1514 && let Some(obj) = v.as_object_mut()
1515 {
1516 obj.insert("proxied_from".into(), json!(tool));
1517 return v;
1518 }
1519 }
1520 local
1521}
1522
1523fn read_index() -> Option<Value> {
1525 let path = if let Some(p) = std::env::var_os("AGENTSTATE_HELP_INDEX") {
1526 std::path::PathBuf::from(p)
1527 } else {
1528 std::path::PathBuf::from(std::env::var_os("HOME")?)
1529 .join(".config/agentstate/help-index.json")
1530 };
1531 std::fs::read_to_string(path)
1532 .ok()
1533 .and_then(|s| serde_json::from_str(&s).ok())
1534}
1535
1536fn json_invocation(tool: &str, topic: &str) -> Option<(&'static str, Vec<String>)> {
1540 match tool {
1541 "ctx" => Some((
1542 "ctx",
1543 vec![
1544 "help".into(),
1545 topic.into(),
1546 "--format".into(),
1547 "json".into(),
1548 "--no-proxy".into(),
1549 ],
1550 )),
1551 "asd" => Some((
1552 "asd",
1553 vec![
1554 "help".into(),
1555 topic.into(),
1556 "--agent".into(),
1557 "--no-proxy".into(),
1558 ],
1559 )),
1560 _ => None,
1561 }
1562}
1563
1564pub fn manifest() -> Value {
1566 json!({
1567 "tool": OWNER,
1568 "version": version(),
1569 "features": REGISTRY.iter().map(|d| json!({
1570 "feature": d.feature,
1571 "synopsis": d.synopsis,
1572 "group": d.group,
1573 })).collect::<Vec<_>>(),
1574 })
1575}
1576
1577#[cfg(test)]
1578mod tests {
1579 use super::*;
1580
1581 #[test]
1582 fn exact_feature_name_wins() {
1583 let v = respond(Some("impact"));
1584 assert_eq!(v["feature"], "impact");
1585 assert_eq!(v["owner"], "asd");
1586 assert!(v["help_tokens"].as_u64().unwrap() > 0);
1587 }
1588
1589 #[test]
1590 fn strong_name_match_returns_single_doc() {
1591 let v = respond(Some("prepare change"));
1592 assert_eq!(v["feature"], "prepare_change");
1593 }
1594
1595 #[test]
1596 fn weak_phrase_disambiguates_not_guesses() {
1597 let v = respond(Some("understand the code"));
1598 assert!(
1599 v.get("feature").is_none(),
1600 "should not guess a single doc: {v}"
1601 );
1602 assert!(
1603 v["matches"].is_array() || v["not_found"].is_string(),
1604 "got {v}"
1605 );
1606 }
1607
1608 #[test]
1609 fn empty_topic_returns_catalog() {
1610 let v = respond(None);
1611 assert_eq!(
1612 v["feature_count"].as_u64().unwrap() as usize,
1613 REGISTRY.len()
1614 );
1615 assert!(v["groups"].is_object());
1616 }
1617
1618 #[test]
1619 fn manifest_lists_every_feature() {
1620 let m = manifest();
1621 assert_eq!(m["tool"], "asd");
1622 assert_eq!(m["features"].as_array().unwrap().len(), REGISTRY.len());
1623 }
1624}