agent-doc-system 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/INSTALL_FOR_AGENTS.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +151 -2
  4. package/adapters/cache.mjs +27 -0
  5. package/adapters/contract-consumers.mjs +56 -0
  6. package/adapters/database.mjs +116 -0
  7. package/adapters/deploy-config.mjs +34 -0
  8. package/adapters/dockerfile.mjs +26 -0
  9. package/adapters/events.mjs +145 -0
  10. package/adapters/generic.mjs +86 -0
  11. package/adapters/github-actions.mjs +106 -0
  12. package/adapters/go.mjs +91 -0
  13. package/adapters/graphql.mjs +28 -0
  14. package/adapters/index.mjs +55 -0
  15. package/adapters/node.mjs +118 -0
  16. package/adapters/object-store.mjs +64 -0
  17. package/adapters/oidc.mjs +56 -0
  18. package/adapters/openapi.mjs +50 -0
  19. package/adapters/pnpm.mjs +42 -0
  20. package/adapters/protobuf.mjs +35 -0
  21. package/adapters/registry.mjs +27 -0
  22. package/adapters/resource-client.mjs +61 -0
  23. package/adapters/turborepo.mjs +135 -0
  24. package/adapters/typescript.mjs +85 -0
  25. package/adapters/wrangler.mjs +206 -0
  26. package/bin/agentdoc.mjs +478 -0
  27. package/bin/usage.txt +26 -0
  28. package/core/acceptances.mjs +69 -0
  29. package/core/audit.mjs +407 -0
  30. package/core/authority/classes.mjs +39 -0
  31. package/core/authority/engine.mjs +243 -0
  32. package/core/authority/facts.mjs +41 -0
  33. package/core/codes.mjs +119 -0
  34. package/core/compile.mjs +881 -0
  35. package/core/contracts.mjs +121 -0
  36. package/core/derive.mjs +102 -0
  37. package/core/descriptors.mjs +486 -0
  38. package/core/determinism.mjs +95 -0
  39. package/core/discover.mjs +147 -0
  40. package/core/facts.mjs +53 -0
  41. package/core/fsx.mjs +342 -0
  42. package/core/graph.mjs +260 -0
  43. package/core/impact.mjs +175 -0
  44. package/core/indexes.mjs +165 -0
  45. package/core/jsonschema.mjs +298 -0
  46. package/core/observations.mjs +129 -0
  47. package/core/provenance.mjs +83 -0
  48. package/core/query.mjs +384 -0
  49. package/core/relations.mjs +286 -0
  50. package/core/scaffold.mjs +322 -0
  51. package/core/secrets.mjs +92 -0
  52. package/core/sourcescan.mjs +88 -0
  53. package/core/toml.mjs +163 -0
  54. package/core/yaml.mjs +475 -0
  55. package/docs/ADAPTERS.md +172 -0
  56. package/docs/AUTHORITY.md +260 -0
  57. package/docs/CI.md +136 -0
  58. package/docs/COMPARISON-KODA.md +164 -0
  59. package/docs/EVALS.md +166 -0
  60. package/docs/MIGRATION.md +193 -0
  61. package/docs/PROVENANCE.md +133 -0
  62. package/docs/QUERY.md +180 -0
  63. package/docs/SPEC.md +339 -0
  64. package/docs/VERIFICATION.md +28 -0
  65. package/docs/ci/generic-ci.sh +28 -0
  66. package/docs/ci/github-actions.yml +35 -0
  67. package/evals/harness.mjs +426 -0
  68. package/evals/scenarios/fixture-a-01-change-api.json +9 -0
  69. package/evals/scenarios/fixture-a-02-change-db.json +9 -0
  70. package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
  71. package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
  72. package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
  73. package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
  74. package/evals/scenarios/fixture-b-04-journey.json +9 -0
  75. package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
  76. package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
  77. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
  78. package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
  79. package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
  80. package/evals/scenarios/koda-02-change-db-schema.json +20 -0
  81. package/evals/scenarios/koda-03-change-auth.json +21 -0
  82. package/evals/scenarios/koda-04-add-event-producer.json +19 -0
  83. package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
  84. package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
  85. package/evals/scenarios/koda-07-change-shared-package.json +16 -0
  86. package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
  87. package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
  88. package/evals/thresholds.json +11 -0
  89. package/package.json +46 -4
  90. package/schemas/api.schema.json +84 -0
  91. package/schemas/common.schema.json +107 -0
  92. package/schemas/component.schema.json +40 -0
  93. package/schemas/config.schema.json +685 -0
  94. package/schemas/domain.schema.json +14 -0
  95. package/schemas/graph.schema.json +1084 -0
  96. package/schemas/journeys.schema.json +47 -0
  97. package/schemas/observation.schema.json +143 -0
  98. package/schemas/resource.schema.json +28 -0
  99. package/schemas/system.schema.json +26 -0
  100. package/skills/agent-doc-system/SKILL.md +113 -0
  101. package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
  102. package/skills/agent-doc-system/references/cli.md +89 -0
  103. package/skills/agent-doc-system/references/create-migrate.md +101 -0
  104. package/skills/agent-doc-system/references/maintenance.md +52 -0
  105. package/templates/ADR.md +26 -0
  106. package/templates/ARCHITECTURE.md +19 -0
  107. package/templates/CONSTRAINTS.md +17 -0
  108. package/templates/PRODUCT.md +17 -0
  109. package/templates/api.yaml +10 -0
  110. package/templates/ci.yml +31 -0
  111. package/templates/component.yaml +14 -0
  112. package/templates/domain.yaml +6 -0
  113. package/templates/journey.yaml +7 -0
  114. package/templates/observation.yaml +19 -0
  115. package/templates/resource.yaml +8 -0
  116. package/templates/system.yaml +10 -0
  117. package/templates/warning-acceptance.yaml +14 -0
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env node
2
+ // agentdoc — portable documentation & software-catalog system.
3
+ //
4
+ // The command list lives in bin/usage.txt and is printed from there, so this
5
+ // header, `--help` and the shipped file cannot drift into three different
6
+ // claims about what the tool does.
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { AgentDocError, CODES } from "../core/codes.mjs";
11
+ import { compile, sha256Hex } from "../core/compile.mjs";
12
+ import { Repo } from "../core/fsx.mjs";
13
+ import { assertFresh, readGraph, COMPILER_VERSION, GRAPH_SCHEMA_VERSION, COMPILER_NAME } from "../core/graph.mjs";
14
+ import { queryContext, renderContext } from "../core/query.mjs";
15
+ import { impact } from "../core/impact.mjs";
16
+ import { audit, scaffoldProposal } from "../core/audit.mjs";
17
+ import { observationFreshness, stalenessDiagnostics } from "../core/observations.mjs";
18
+ import { assertNoSecretsInText } from "../core/secrets.mjs";
19
+ import { installTemplates } from "../core/scaffold.mjs";
20
+ import { loadSchemaBundle, loadConfig } from "../core/descriptors.mjs";
21
+ import { execFileSync } from "node:child_process";
22
+
23
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
24
+ const PACKAGE_ROOT = path.join(HERE, "..");
25
+ const VERSION = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version;
26
+ const MIN_NODE_MAJOR = 22;
27
+ // Commands are run from wherever the agent happens to be. Walk up until the
28
+ // configuration is found, so `agentdoc query src/foo.ts` works from a
29
+ // subdirectory instead of failing with a misleading missing-file error.
30
+ function findRoot(start) {
31
+ let dir = start;
32
+ for (let i = 0; i < 24; i++) {
33
+ if (fs.existsSync(path.join(dir, "agentdoc", "agentdoc.config.yaml"))) return dir;
34
+ const up = path.dirname(dir);
35
+ if (up === dir) break;
36
+ dir = up;
37
+ }
38
+ return start;
39
+ }
40
+
41
+ const ROOT = findRoot(process.cwd());
42
+ const argv = process.argv.slice(2);
43
+ const cmd = argv[0];
44
+ // Flags that consume the next token as a value. Anything else is boolean; a
45
+ // `--flag value` pair must not leak the value into `positional`
46
+ // (`agentdoc query --budget relations=10 svc/api` must query svc/api).
47
+ const VALUE_FLAGS = new Set(["budget", "diff", "from"]);
48
+ const flags = new Set();
49
+ const positional = [];
50
+ for (let i = 1; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a.startsWith("--")) {
53
+ const name = a.replace(/^--/, "").split("=")[0];
54
+ if (a.includes("=") && !VALUE_FLAGS.has(name)) {
55
+ console.error("agentdoc: --" + name + " does not take a value");
56
+ process.exit(2);
57
+ }
58
+ flags.add(name);
59
+ if (!a.includes("=") && VALUE_FLAGS.has(name) && argv[i + 1] && !argv[i + 1].startsWith("--")) i++;
60
+ continue;
61
+ }
62
+ positional.push(a);
63
+ }
64
+ const flag = (name, dflt = null) => {
65
+ for (let i = 0; i < argv.length; i++) {
66
+ if (argv[i] === "--" + name) {
67
+ const next = argv[i + 1];
68
+ return next && !next.startsWith("--") ? next : true;
69
+ }
70
+ if (argv[i].startsWith("--" + name + "=")) return argv[i].slice(name.length + 3);
71
+ }
72
+ return dflt;
73
+ };
74
+
75
+ function fail(errors, stats) {
76
+ for (const e of errors) console.error("ERROR " + (e.format ? e.format() : e.code + ": " + e.message));
77
+ if (stats) console.error("agentdoc: " + JSON.stringify(stats));
78
+ process.exit(1);
79
+ }
80
+
81
+ function usage(exitCode) {
82
+ const text = fs.readFileSync(path.join(HERE, "usage.txt"), "utf8");
83
+ (exitCode === 0 ? process.stdout : process.stderr).write(text);
84
+ process.exit(exitCode);
85
+ }
86
+
87
+ function printDiagnostics(res) {
88
+ for (const d of res.diagnostics || []) {
89
+ const prefix = d.severity === "error" ? "ERROR " : d.acceptance ? "WARN ACCEPTED " : "WARN ";
90
+ console.log(prefix + d.code + ": " + d.message);
91
+ if (d.acceptance) console.log(" " + d.acceptance.classification + ": " + d.acceptance.reason);
92
+ }
93
+ }
94
+
95
+ function gateObservationFreshness(res) {
96
+ // Time-dependent gates run before artifact gates. A committed graph can be
97
+ // stale for many reasons; when the cause is an observation that has aged out
98
+ // of its promise, saying "rebuild the graph" sends the reader down the wrong
99
+ // path entirely.
100
+ const abs = path.join(ROOT, res.graphPath);
101
+ if (!fs.existsSync(abs)) return null;
102
+ const { graph } = readGraph(res.repo, res.graphPath);
103
+ const fresh = observationFreshness(graph.observations || [], new Date());
104
+ const stale = stalenessDiagnostics(fresh);
105
+ if (!stale.length) return fresh;
106
+ if (!flags.has("allow-stale-observations")) {
107
+ for (const d of stale) console.error("ERROR " + d.code + ": " + d.message);
108
+ process.exit(1);
109
+ }
110
+ // Staleness was explicitly allowed through. This is where a rule's
111
+ // degradeOnStale is applied — at gate time, never at compile time, so a
112
+ // compile stays a pure function of the checkout. `conflict` keeps the
113
+ // election and warns; `unresolved` refuses to stand on a stale observation.
114
+ // Match the elected assertion to the stale observation *set* by capturedAt
115
+ // + environment — capturedAt alone can collide across sets.
116
+ const staleSets = new Set(fresh.filter((f) => f.stale).map((f) => f.capturedAt + "\u0000" + f.environment));
117
+ const assertionById = new Map((graph.assertions || []).map((a) => [a.id, a]));
118
+ const rules = new Map((((res.cfg || {}).authority || {}).rules || []).map((r) => [r.id, r]));
119
+ const degraded = [];
120
+ for (const c of graph.conflicts || []) {
121
+ if (c.status !== "resolved" || !c.election || !c.election.electedAssertionId) continue;
122
+ const rule = rules.get(c.election.ruleId);
123
+ if (!rule || !rule.degradeOnStale) continue;
124
+ const elected = assertionById.get(c.election.electedAssertionId);
125
+ const obs = elected && elected.observed;
126
+ if (!obs || !obs.at || !staleSets.has(obs.at + "\u0000" + obs.environment)) continue;
127
+ if (rule.degradeOnStale === "unresolved") {
128
+ degraded.push(
129
+ c.subject + " " + c.key + " — the elected observation (" + obs.at + ") is past its maxAgeDays " +
130
+ "and authority rule " + rule.id + " requires the election to degrade to unresolved"
131
+ );
132
+ } else {
133
+ console.error(
134
+ "WARN " + CODES.ELECTION_STALE + ": " + c.subject + " " + c.key +
135
+ " — the elected observation (" + obs.at + ") is past its maxAgeDays; authority rule " +
136
+ rule.id + " keeps the election"
137
+ );
138
+ }
139
+ }
140
+ if (degraded.length) {
141
+ for (const m of degraded) console.error("ERROR " + CODES.CONFLICT_UNRESOLVED + ": " + m);
142
+ process.exit(1);
143
+ }
144
+ return fresh;
145
+ }
146
+
147
+ function requireFresh(res, fresh) {
148
+ const abs = path.join(ROOT, res.graphPath);
149
+ if (!fs.existsSync(abs)) {
150
+ fail([new AgentDocError(CODES.GRAPH_MISSING, "no compiled graph at " + res.graphPath + " — run `agentdoc compile`")]);
151
+ }
152
+ const { graph, text } = readGraph(res.repo, res.graphPath);
153
+ try {
154
+ assertFresh(graph, {
155
+ inputHash: res.stats.inputHash,
156
+ dirty: res.stats.dirty,
157
+ compilerVersion: COMPILER_VERSION,
158
+ schemaVersion: GRAPH_SCHEMA_VERSION,
159
+ });
160
+ } catch (e) {
161
+ fail([e]);
162
+ }
163
+ if (graph.source.dirty && (flags.has("require-clean") || flag("require-clean"))) {
164
+ fail([new AgentDocError(
165
+ CODES.DIRTY_SOURCE,
166
+ "catalog inputs differ from HEAD (" + res.stats.commit + ") — CI/publish evidence requires a clean source; commit the change and rebuild"
167
+ )], res.stats);
168
+ }
169
+ // `fresh` is normally precomputed by the caller — gateObservationFreshness
170
+ // also evaluates election degradation, so calling it twice would print the
171
+ // same staleness warning twice. The fallback keeps a forgotten call-site safe.
172
+ return { graph, text, fresh: fresh === undefined ? (gateObservationFreshness(res) || []) : fresh };
173
+ }
174
+
175
+ function out(obj, asJson) {
176
+ const text = asJson ? JSON.stringify(obj, null, 2) + "\n" : null;
177
+ if (text) assertNoSecretsInText(text, "output");
178
+ process.stdout.write(text || obj + "\n");
179
+ }
180
+
181
+ try {
182
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
183
+ console.log("agentdoc " + VERSION);
184
+ process.exit(0);
185
+ }
186
+ if (cmd === "--help" || cmd === "-h" || cmd === "help" || flags.has("help") || argv.includes("-h")) {
187
+ usage(0);
188
+ }
189
+ if (cmd === "validate") {
190
+ const res = compile(ROOT);
191
+ printDiagnostics(res);
192
+ if (res.errors.length) fail(res.errors, res.stats);
193
+ console.log("agentdoc validate: PASS " + JSON.stringify(res.stats));
194
+ } else if (cmd === "compile") {
195
+ const res = compile(ROOT);
196
+ printDiagnostics(res);
197
+ if (res.errors.length) fail(res.errors, res.stats);
198
+ if ((flags.has("require-clean") || flag("require-clean")) && res.stats.dirty) {
199
+ fail([new AgentDocError(CODES.DIRTY_SOURCE, "catalog inputs differ from HEAD — commit the change and rebuild")], res.stats);
200
+ }
201
+ fs.mkdirSync(path.dirname(path.join(ROOT, res.graphPath)), { recursive: true });
202
+ const tmp = path.join(ROOT, res.graphPath) + ".tmp-" + process.pid;
203
+ fs.writeFileSync(tmp, res.serialized);
204
+ fs.renameSync(tmp, path.join(ROOT, res.graphPath));
205
+ console.log("agentdoc compile: PASS " + JSON.stringify(res.stats));
206
+ console.log("graph: " + res.graphPath);
207
+ } else if (cmd === "check") {
208
+ const res = compile(ROOT);
209
+ printDiagnostics(res);
210
+ if (res.errors.length) fail(res.errors, res.stats);
211
+ const preFresh = gateObservationFreshness(res);
212
+ const { graph, text, fresh } = requireFresh(res, preFresh);
213
+ // `source.commit` records which checkout produced the graph, and a checkout
214
+ // without VCS — an extracted archive, a vendored dependency — compiles to
215
+ // `0000000`. That is a difference in provenance, not in content, and failing
216
+ // on it would make a committed graph unusable in exactly the situation it
217
+ // is most needed: shipped as a file, with no history. Everything else must
218
+ // still match byte for byte.
219
+ const withoutCommit = (t) => t.replace(/^(\s*)"commit": "[0-9a-f]*",$/m, '$1"commit": "<provenance>",');
220
+ if (withoutCommit(text) !== withoutCommit(res.serialized)) {
221
+ fail([new AgentDocError(
222
+ CODES.STALE_GRAPH,
223
+ "compiled graph differs from a fresh deterministic compile — commit the descriptor or authority change and rebuild"
224
+ )]);
225
+ }
226
+ const gj = JSON.stringify(graph, null, 2) + "\n";
227
+ if (gj !== text) {
228
+ fail([new AgentDocError(CODES.STALE_GRAPH, "compiled graph is not canonically serialized — rebuild the graph")]);
229
+ }
230
+ console.log("agentdoc check: PASS " + JSON.stringify({ ...res.stats, observationFreshness: fresh.length }));
231
+ } else if (cmd === "query") {
232
+ const needle = positional[0];
233
+ if (!needle) { console.error("usage: agentdoc query <repo-path|entity-ref> [--json] [--md] [--budget relations=10,verification=4]"); process.exit(2); }
234
+ const res = compile(ROOT);
235
+ if (res.errors.length) fail(res.errors, res.stats);
236
+ // Time-dependent gates first, for the same reason as `check`: a stale
237
+ // observation is a different problem from a stale graph and sends the reader
238
+ // somewhere else entirely.
239
+ const preFresh = gateObservationFreshness(res);
240
+ const { graph } = requireFresh(res, preFresh);
241
+ // `--budget` exists because the default budgets are generous enough that a
242
+ // focused query never truncates, which makes the truncation contract
243
+ // untestable and unreachable from the CLI. An agent working in a tight
244
+ // context needs to be able to say so.
245
+ let budget = {};
246
+ const raw = flag("budget");
247
+ if (raw === true || raw === "") {
248
+ console.error("usage: --budget section=limit,... (limits are positive integers)");
249
+ process.exit(2);
250
+ }
251
+ if (raw) {
252
+ for (const pair of raw.split(",")) {
253
+ const [k, v] = pair.split("=");
254
+ if (!k || !/^\d+$/.test(String(v))) {
255
+ console.error("usage: --budget section=limit,... (limits are positive integers)");
256
+ process.exit(2);
257
+ }
258
+ budget[k.trim()] = Number(v);
259
+ }
260
+ }
261
+ const ctx = queryContext(graph, needle, { budget });
262
+ if (flags.has("md") || flag("md")) out(renderContext(ctx));
263
+ else if (flags.has("json")) out(ctx, true);
264
+ else out(renderContext(ctx));
265
+ } else if (cmd === "impact") {
266
+ // Impact answers a question about the working tree, so it compiles in
267
+ // memory rather than reading the committed graph. Refusing a stale graph
268
+ // would be right for `query` (which feeds a prompt) and wrong here: the
269
+ // whole point of impact is to run *before* you rebuild.
270
+ const res = compile(ROOT);
271
+ if (res.errors.length) fail(res.errors, res.stats);
272
+ const graph = res.graph;
273
+ const fromRef = flag("diff") || flag("from");
274
+ let paths = positional;
275
+ if (fromRef && fromRef !== true) {
276
+ if (!res.repo.hasGit()) {
277
+ console.error("agentdoc impact --diff requires a git repository; this checkout has no VCS");
278
+ process.exit(2);
279
+ }
280
+ paths = res.repo.diffPaths(fromRef);
281
+ if (paths === null) {
282
+ console.error("cannot resolve diff ref '" + fromRef + "' — is it a valid commit, branch or range?");
283
+ process.exit(2);
284
+ }
285
+ if (!paths.length) {
286
+ console.error("no changed files between " + fromRef + " and HEAD");
287
+ process.exit(2);
288
+ }
289
+ }
290
+ if (!paths.length) {
291
+ console.error("usage: agentdoc impact <path...> | agentdoc impact --diff <git-ref>");
292
+ process.exit(2);
293
+ }
294
+ out(impact(graph, paths), true);
295
+ } else if (cmd === "audit") {
296
+ const res = compile(ROOT);
297
+ if (!res.graph) {
298
+ // An audit must work on a repository that cannot compile yet; report the
299
+ // compilation failures as part of the audit rather than aborting.
300
+ const report = {
301
+ compileErrors: res.errors.map((e) => ({ code: e.code, message: e.message, path: e.path || null, ref: e.ref || null })),
302
+ stats: res.stats,
303
+ };
304
+ out(report, true);
305
+ process.exit(0);
306
+ }
307
+ const report = audit(res.repo, res.cfg, res);
308
+ report.scaffold = scaffoldProposal(res.repo, res.cfg, report);
309
+ out(report, true);
310
+ } else if (cmd === "scaffold") {
311
+ const res = compile(ROOT);
312
+ const write = flags.has("write") || flag("write");
313
+ // Scaffolding is how a repository reaches a compiling state: requiring the
314
+ // compile to succeed first deadlocks CREATE on every repository whose units
315
+ // are discovered by adapters (go.work, pnpm-workspace, supplementalRoots)
316
+ // rather than already carrying descriptors. When a usable configuration
317
+ // exists, write what can be proven and report what still blocks the compile.
318
+ if (!res.cfg) {
319
+ out({
320
+ scaffolded: [],
321
+ blocked: res.errors.map((e) => ({ code: e.code, message: e.message })),
322
+ note: "the repository cannot compile yet; install the configuration first with `agentdoc init`",
323
+ }, true);
324
+ process.exit(1);
325
+ }
326
+ const report = res.graph ? audit(res.repo, res.cfg, res) : null;
327
+ const proposal = report ? scaffoldProposal(res.repo, res.cfg, report) : null;
328
+ const result = installTemplates(res.repo, res.cfg, {
329
+ write: Boolean(write),
330
+ eligible: res.discovery && res.discovery.eligible,
331
+ });
332
+ out({
333
+ ...result,
334
+ ...(proposal ? { proposal } : {}),
335
+ ...(res.errors.length
336
+ ? { blocked: res.errors.map((e) => ({ code: e.code, message: e.message })) }
337
+ : {}),
338
+ }, true);
339
+ } else if (cmd === "init") {
340
+ const result = installTemplates(null, null, { write: true, root: ROOT, force: flags.has("force") });
341
+ out(result, true);
342
+ } else if (cmd === "eval") {
343
+ const which = positional[0] || "routing";
344
+ if (which !== "routing") { console.error("usage: agentdoc eval routing [--json]"); process.exit(2); }
345
+ // The harness ships, but it is loaded lazily so a trimmed or damaged
346
+ // installation cannot make every other command fail on a module the user
347
+ // never asked for.
348
+ let runRoutingEval;
349
+ try {
350
+ ({ runRoutingEval } = await import("../evals/harness.mjs"));
351
+ } catch (e) {
352
+ if (e && e.code === "ERR_MODULE_NOT_FOUND") {
353
+ console.error("agentdoc eval: the evaluation harness is missing from this installation — reinstall the agentdoc CLI, or run it from a source checkout");
354
+ process.exit(1);
355
+ }
356
+ throw e;
357
+ }
358
+ const report = runRoutingEval(ROOT, { json: flags.has("json") });
359
+ if (flags.has("json")) { out(report, true); process.exit(report.verdict === "FAIL" ? 1 : 0); }
360
+ else {
361
+ console.log(JSON.stringify(report.metrics, null, 2));
362
+ console.log("thresholds: " + JSON.stringify(report.thresholds));
363
+ console.log("verdict: " + report.verdict + " (" + report.failures.join("; ") + ")");
364
+ }
365
+ if (report.verdict === "FAIL") process.exit(1);
366
+ } else if (cmd === "pin-evidence") {
367
+ // Reviewed overrides pin their evidence by content hash, and those digests
368
+ // are re-verified on every compile. That is only workable if an author can
369
+ // obtain the current digest without computing SHA-256 by hand — otherwise
370
+ // the discipline quietly becomes "delete the override", which is worse
371
+ // than not requiring it.
372
+ // Read the configuration directly rather than through `compile`: a stale
373
+ // digest is precisely the condition this command exists to report, and it
374
+ // must be reportable when `compile` refuses to run.
375
+ const repo = new Repo(ROOT);
376
+ const cfg = loadConfig(repo, loadSchemaBundle().bundle);
377
+ const results = [];
378
+ for (const o of cfg.reviewedOverrides || []) {
379
+ for (const ev of o.evidence || []) {
380
+ const p = path.join(ROOT, ev.path);
381
+ if (!fs.existsSync(p)) {
382
+ results.push({ subject: o.subject, path: ev.path, current: null, pinned: ev.sha256, status: "MISSING" });
383
+ continue;
384
+ }
385
+ const current = sha256Hex(fs.readFileSync(p));
386
+ results.push({
387
+ subject: o.subject, path: ev.path, current,
388
+ pinned: ev.sha256,
389
+ status: current === ev.sha256 ? "CURRENT" : "STALE",
390
+ });
391
+ }
392
+ }
393
+ if (flags.has("json")) { out({ evidence: results }, true); }
394
+ else {
395
+ for (const r of results) {
396
+ console.log(r.status.padEnd(8) + " " + r.path);
397
+ if (r.status === "STALE") {
398
+ console.log(" pinned: " + r.pinned);
399
+ console.log(" current: " + r.current + " <- update the digest after reviewing the change");
400
+ }
401
+ }
402
+ console.log(results.length + " evidence entries across " + (cfg.reviewedOverrides || []).length + " reviewed overrides");
403
+ }
404
+ if (results.some((r) => r.status !== "CURRENT")) process.exit(1);
405
+ } else if (cmd === "doctor") {
406
+ // Two scopes, kept explicit: `doctor` is a machine check — is this
407
+ // installation of the CLI itself healthy — and never fails because a
408
+ // project happens not to be configured yet. `doctor --project` adds the
409
+ // project checks and fails on those too.
410
+ const major = Number(process.versions.node.split(".")[0]);
411
+ const machine = {
412
+ agentdoc: VERSION,
413
+ node: process.version,
414
+ nodeSupported: major >= MIN_NODE_MAJOR,
415
+ nodeRequired: ">=" + MIN_NODE_MAJOR,
416
+ compiler: COMPILER_NAME + "@" + COMPILER_VERSION,
417
+ graphSchema: GRAPH_SCHEMA_VERSION,
418
+ installRoot: PACKAGE_ROOT,
419
+ schemaBundle: null,
420
+ gitAvailable: false,
421
+ externalRuntimeDependencies: [],
422
+ };
423
+ try {
424
+ const { versionInputs } = loadSchemaBundle();
425
+ machine.schemaBundle = { ok: true, schemas: versionInputs.length };
426
+ } catch (e) {
427
+ machine.schemaBundle = { ok: false, error: (e.code ? e.code + ": " : "") + e.message };
428
+ }
429
+ try {
430
+ execFileSync("git", ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
431
+ machine.gitAvailable = true;
432
+ } catch {
433
+ machine.gitAvailable = false;
434
+ }
435
+ const machineOk = machine.nodeSupported && machine.schemaBundle.ok;
436
+
437
+ const report = { scope: "machine", ok: machineOk, machine };
438
+ if (flags.has("project")) {
439
+ const configFound = fs.existsSync(path.join(ROOT, "agentdoc", "agentdoc.config.yaml"));
440
+ const project = {
441
+ root: ROOT,
442
+ configurationFound: configFound,
443
+ configPath: "agentdoc/agentdoc.config.yaml",
444
+ writable: null,
445
+ graphPresent: false,
446
+ };
447
+ try {
448
+ fs.accessSync(ROOT, fs.constants.W_OK);
449
+ project.writable = true;
450
+ } catch {
451
+ project.writable = false;
452
+ }
453
+ if (configFound) {
454
+ try {
455
+ const cfg = loadConfig(new Repo(ROOT), loadSchemaBundle().bundle);
456
+ project.graphPath = cfg.output?.graph || ".agentdoc/graph.json";
457
+ project.graphPresent = fs.existsSync(path.join(ROOT, project.graphPath));
458
+ } catch (e) {
459
+ project.configError = (e.code ? e.code + ": " : "") + e.message;
460
+ }
461
+ }
462
+ report.scope = "machine+project";
463
+ report.project = project;
464
+ report.ok = machineOk && configFound && project.writable === true && !project.configError;
465
+ }
466
+ out(report, true);
467
+ if (!report.ok) process.exit(1);
468
+ } else {
469
+ usage(2);
470
+ }
471
+ } catch (e) {
472
+ if (e instanceof AgentDocError) {
473
+ console.error("ERROR " + e.format());
474
+ process.exit(1);
475
+ }
476
+ console.error("agentdoc internal error: " + (e && e.stack ? e.stack : String(e)));
477
+ process.exit(1);
478
+ }
package/bin/usage.txt ADDED
@@ -0,0 +1,26 @@
1
+ agentdoc — portable documentation & software-catalog system
2
+
3
+ agentdoc --version print the installed version
4
+ agentdoc --help show this help (also: agentdoc <command> --help)
5
+
6
+ agentdoc init [--force] write a configuration and documentation skeleton
7
+ agentdoc audit inventory the repository, classify every fact
8
+ agentdoc scaffold [--write] generate a CREATE-mode skeleton from the audit
9
+ agentdoc validate sources + schemas + semantic checks
10
+ agentdoc compile [--require-clean] deterministic graph build (atomic write)
11
+ agentdoc check [--require-clean] [--allow-stale-observations]
12
+ freshness, determinism and gate check
13
+ agentdoc query <path|ref> [--md] [--json] [--budget k=n]
14
+ route minimal context for a task
15
+ agentdoc impact <paths...> what a change forces you to keep consistent
16
+ agentdoc impact --diff <git-ref> the same, for a commit range
17
+ agentdoc eval routing [--json] agent-routing evaluation against registered
18
+ scenarios (SKIP when the repo has none)
19
+ agentdoc pin-evidence [--json] verify digests pinned by reviewed overrides
20
+ agentdoc doctor machine self-check: is this CLI install healthy
21
+ agentdoc doctor --project the same, plus the current project's catalog health
22
+
23
+ Commands that operate on a catalog run from anywhere inside the repository;
24
+ the configuration is found by walking up to agentdoc/agentdoc.config.yaml.
25
+
26
+ Exit codes: 0 ok, 1 failure, 2 usage.
@@ -0,0 +1,69 @@
1
+ // Explicit warning dispositions.
2
+ //
3
+ // An accepted warning is not an ignored warning. It carries a typed identity, a
4
+ // classification, a reason, a review condition, the exact observed refs it was
5
+ // written against, and content hashes of the evidence. If the evidence changes,
6
+ // the acceptance stops matching and compilation fails: a review is forced, not
7
+ // inherited. Acceptance can never downgrade an error.
8
+ import { AgentDocError, CODES, ACCEPTABLE_WARNING_CODES } from "./codes.mjs";
9
+ import { assertRepoPath, sha256Hex } from "./fsx.mjs";
10
+ import { scanForSecrets } from "./secrets.mjs";
11
+ import { CONFIG_PATH } from "./descriptors.mjs";
12
+
13
+ export function applyWarningAcceptances(repo, acceptances, diagnostics) {
14
+ const errors = [];
15
+ const seen = new Set();
16
+ for (const acceptance of acceptances) {
17
+ try {
18
+ const { code, subject, observedRefs, evidence } = acceptance;
19
+ if (!ACCEPTABLE_WARNING_CODES.includes(code)) {
20
+ throw new AgentDocError(
21
+ CODES.WARNING_ACCEPTANCE,
22
+ "warning code " + code + " is not acceptable; only " + ACCEPTABLE_WARNING_CODES.join(", ") + " may be accepted",
23
+ { path: CONFIG_PATH }
24
+ );
25
+ }
26
+ const key = code + "|" + subject;
27
+ if (seen.has(key)) throw new AgentDocError(CODES.WARNING_ACCEPTANCE, "duplicate warning acceptance for " + key, { path: CONFIG_PATH });
28
+ seen.add(key);
29
+ scanForSecrets(acceptance, CONFIG_PATH);
30
+
31
+ const matches = diagnostics.filter((d) => d.code === code && d.subject === subject);
32
+ if (matches.length !== 1 || matches[0].severity !== "warning") {
33
+ throw new AgentDocError(
34
+ CODES.WARNING_ACCEPTANCE,
35
+ "warning acceptance must match exactly one current warning: " + key + " — remove it or review the underlying fact",
36
+ { path: CONFIG_PATH }
37
+ );
38
+ }
39
+ const diagnostic = matches[0];
40
+ if (JSON.stringify([...observedRefs].sort()) !== JSON.stringify([...(diagnostic.refs || [])].sort())) {
41
+ throw new AgentDocError(
42
+ CODES.WARNING_ACCEPTANCE,
43
+ "observed refs changed for " + key + " — re-review the current consumers or component",
44
+ { path: CONFIG_PATH }
45
+ );
46
+ }
47
+ const paths = new Set();
48
+ for (const item of evidence) {
49
+ assertRepoPath(item.path, CONFIG_PATH);
50
+ if (paths.has(item.path)) {
51
+ throw new AgentDocError(CODES.WARNING_ACCEPTANCE, "duplicate acceptance evidence path: " + item.path, { path: CONFIG_PATH });
52
+ }
53
+ paths.add(item.path);
54
+ if (sha256Hex(repo.readBytes(item.path)) !== item.sha256) {
55
+ throw new AgentDocError(
56
+ CODES.WARNING_ACCEPTANCE,
57
+ "warning acceptance evidence changed: " + item.path + " — review the disposition before updating its digest",
58
+ { path: CONFIG_PATH }
59
+ );
60
+ }
61
+ }
62
+ diagnostic.acceptance = acceptance;
63
+ } catch (e) {
64
+ if (!(e instanceof AgentDocError)) throw e;
65
+ errors.push(e);
66
+ }
67
+ }
68
+ return errors;
69
+ }