agent-doc-system 1.0.0 → 1.2.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 (68) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +97 -112
  3. package/adapters/ansible.mjs +306 -0
  4. package/adapters/cache.mjs +8 -2
  5. package/adapters/contract-consumers.mjs +1 -1
  6. package/adapters/database.mjs +18 -4
  7. package/adapters/events.mjs +1 -1
  8. package/adapters/github-actions.mjs +244 -85
  9. package/adapters/go.mjs +1 -1
  10. package/adapters/index.mjs +5 -2
  11. package/adapters/object-store.mjs +2 -2
  12. package/adapters/python.mjs +1142 -0
  13. package/adapters/resource-client.mjs +1 -1
  14. package/adapters/rust.mjs +987 -0
  15. package/adapters/typescript.mjs +1 -1
  16. package/bin/agentdoc.mjs +60 -44
  17. package/bin/commands.mjs +169 -0
  18. package/bin/usage.txt +5 -3
  19. package/core/audit.mjs +55 -4
  20. package/core/authority/engine.mjs +27 -4
  21. package/core/codes.mjs +11 -0
  22. package/core/compile.mjs +263 -13
  23. package/core/dependents.mjs +42 -0
  24. package/core/descriptors.mjs +5 -2
  25. package/core/discover.mjs +60 -9
  26. package/core/fsx.mjs +27 -4
  27. package/core/graph.mjs +1 -1
  28. package/core/impact.mjs +55 -44
  29. package/core/normative.mjs +171 -0
  30. package/core/query.mjs +18 -8
  31. package/core/scaffold.mjs +87 -22
  32. package/core/sourcescan.mjs +11 -2
  33. package/core/toml.mjs +463 -140
  34. package/core/yaml.mjs +5 -3
  35. package/docs/ADAPTERS.md +232 -7
  36. package/docs/AUTHORITY.md +10 -1
  37. package/docs/BENCHMARK.md +68 -0
  38. package/docs/CI.md +3 -1
  39. package/docs/EVALS.md +33 -2
  40. package/docs/MIGRATION.md +76 -6
  41. package/docs/QUERY.md +53 -4
  42. package/docs/SPEC.md +22 -0
  43. package/docs/assets/readme/agentdoc-hero.webp +0 -0
  44. package/docs/assets/readme/agentdoc-in-action.webp +0 -0
  45. package/docs/assets/readme/how-agentdoc-works.webp +0 -0
  46. package/docs/assets/readme/why-agentdoc.webp +0 -0
  47. package/docs/ci/github-actions.yml +1 -1
  48. package/evals/harness.mjs +32 -6
  49. package/evals/scenarios/fixture-b-03-change-shared-lib.json +8 -1
  50. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +7 -1
  51. package/evals/scenarios/fixture-e-01-change-shared-lib.json +14 -0
  52. package/evals/scenarios/fixture-e-02-change-manifest.json +9 -0
  53. package/evals/scenarios/fixture-e-03-local-module.json +9 -0
  54. package/evals/scenarios/fixture-e-04-ambiguous-crate.json +14 -0
  55. package/evals/scenarios/fixture-f-01-change-shared-lib.json +9 -0
  56. package/evals/scenarios/fixture-f-02-change-manifest.json +9 -0
  57. package/evals/scenarios/fixture-f-03-shadowed-import.json +9 -0
  58. package/evals/scenarios/fixture-f-04-nested-workspace.json +9 -0
  59. package/evals/scenarios/fixture-f-05-alembic-database.json +9 -0
  60. package/evals/scenarios/fixture-f-06-unittest-job.json +9 -0
  61. package/package.json +2 -1
  62. package/schemas/common.schema.json +40 -0
  63. package/schemas/component.schema.json +5 -1
  64. package/schemas/config.schema.json +117 -1
  65. package/skills/agent-doc-system/SKILL.md +5 -1
  66. package/skills/agent-doc-system/references/cli.md +12 -4
  67. package/skills/agent-doc-system/references/create-migrate.md +28 -6
  68. package/templates/ci.yml +1 -1
package/docs/SPEC.md CHANGED
@@ -237,6 +237,28 @@ The core has no knowledge of folder names. A directory is a unit when it holds
237
237
  a component descriptor, when an adapter recognises it from a workspace file, or
238
238
  when it is listed in `discovery.supplementalRoots`.
239
239
 
240
+ `init` configures the recursive component descriptor glob `*/**/agentdoc.yaml`,
241
+ so descriptors for adapter-discovered units and units added later are loaded
242
+ without regenerating a path list. The scaffold layout scan supplements its
243
+ shallow marker heuristics with adapter discovery: declared workspace members
244
+ under `examples`, `example`, `test`, or `tests`, or beyond the scan depth, remain
245
+ units. Those names alone do not create units. Scaffold selects artifact types
246
+ from adapter shape: deployable (including both shapes) → `service`, importable
247
+ only → `library`, neither → `other`. Placement remains a human choice.
248
+
249
+ `discovery.notUnitRoots` excludes each listed root and its whole subtree from
250
+ inferred units (adapter markers, adapter `roots()`, and supplemental roots) and
251
+ from scaffold layout scans and descriptor proposals, in both dry run and
252
+ `--write`. The defaults are `agentdoc`, `docs`, `scripts`, `node_modules`, and
253
+ `.github`; a configured list replaces them, and `[]` disables these exclusions.
254
+ An existing authored component descriptor strictly below an excluded root still
255
+ claims that directory as a unit. Its own root must supply the usual adapter
256
+ evidence for its artifact type; the exclusion does not waive corroboration.
257
+ Scaffold preserves that descriptor and never proposes or writes one there.
258
+ A descriptor exactly at an excluded root is refused with
259
+ `AGENTDOC_COMPONENT_UNCORROBORATED` naming the exclusion: remove that root from
260
+ `notUnitRoots` or move the descriptor.
261
+
240
262
  - Every discovered unit must have exactly one descriptor at its root, or
241
263
  `AGENTDOC_COVERAGE_ZERO`.
242
264
  - Two descriptors at one root, or a descriptor no adapter can corroborate, is an
@@ -21,7 +21,7 @@ jobs:
21
21
  with: { node-version: '24' }
22
22
  - name: Install the agentdoc CLI
23
23
  # Unpinned tracks main. For CI reproducibility pin a tag or commit:
24
- # github:lbframe/agent-doc-system#v1.0.0
24
+ # github:lbframe/agent-doc-system#v1.2.0
25
25
  # A compiler upgrade intentionally invalidates committed graphs.
26
26
  run: npm install -g github:lbframe/agent-doc-system
27
27
  - name: Validate and compile
package/evals/harness.mjs CHANGED
@@ -35,6 +35,7 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
35
35
  export function factsFromContext(ctx) {
36
36
  const out = new Set();
37
37
  out.add(ctx.entity.ref);
38
+ for (const n of ctx.neighbours) out.add(n.ref);
38
39
  for (const r of ctx.relations.items) out.add(r.type + ":" + r.other);
39
40
  for (const p of ctx.journeyPeers || []) out.add(p);
40
41
  for (const s2 of ctx.placement.systems) out.add(s2);
@@ -76,7 +77,10 @@ export function criticalFromContext(ctx) {
76
77
  }
77
78
 
78
79
  export function verificationFromContext(ctx) {
79
- return new Set(ctx.verification.items.map((v) => "verify:" + v.command));
80
+ // Commands such as `go test ./...` recur in multiple modules. Scenarios can
81
+ // require a scoped check ID as well as a command to measure dependent checks
82
+ // without letting the library's identical command satisfy the expectation.
83
+ return new Set(ctx.verification.items.flatMap((v) => ["verify:" + v.command, "verify-id:" + v.id]));
80
84
  }
81
85
 
82
86
  // ── baseline: unassisted repository exploration ────────────────────────
@@ -154,7 +158,20 @@ export function fullRepositoryContext(graph) {
154
158
  // Baseline verification: without routing, an agent runs the union of every
155
159
  // check in the repository, because nothing narrows it.
156
160
  function baselineVerification(graph) {
157
- return new Set(graph.verification.map((v) => "verify:" + v.command));
161
+ return new Set([...graph.verification, ...graph.gates].flatMap((v) => ["verify:" + v.command, "verify-id:" + v.id]));
162
+ }
163
+
164
+ export function verificationRecall(retrieved, expected) {
165
+ const { hit, total } = score(retrieved, expected);
166
+ return total ? hit / total : 1;
167
+ }
168
+
169
+ // Count scoped check IDs, not commands: identical commands in different
170
+ // components are separate checks. Gates are repository-scoped checks too.
171
+ export function checkSelectivity(graph, recommendedIds) {
172
+ const all = new Set([...graph.verification, ...graph.gates].map((v) => v.id));
173
+ const recommended = [...all].filter((id) => recommendedIds.has(id)).length;
174
+ return all.size ? recommended / all.size : 1;
158
175
  }
159
176
 
160
177
  function score(retrieved, truth) {
@@ -271,6 +288,10 @@ export function runRoutingEval(root, { json = false } = {}) {
271
288
  const retrieved = new Set();
272
289
  const critical = new Set();
273
290
  const verif = new Set();
291
+ const recommendedIds = new Set();
292
+ const collectChecks = (ctx) => {
293
+ for (const v of [...ctx.verification.items, ...ctx.globalGates]) recommendedIds.add(v.id);
294
+ };
274
295
  let unresolved = 0;
275
296
  for (const t of s.touch) {
276
297
  let ctx;
@@ -285,13 +306,16 @@ export function runRoutingEval(root, { json = false } = {}) {
285
306
  for (const x of factsFromContext(ctx)) retrieved.add(x);
286
307
  for (const x of criticalFromContext(ctx)) critical.add(x);
287
308
  for (const x of verificationFromContext(ctx)) verif.add(x);
309
+ collectChecks(ctx);
288
310
  }
289
311
  // Change impact is part of routing: it is what tells the agent which other
290
312
  // components a change reaches. Its output is legitimately part of context.
291
313
  const im = impact(graph, s.touch);
314
+ for (const v of im.recommendedVerification) recommendedIds.add(v.id);
292
315
  for (const c of im.affectedComponents) {
293
316
  const ctx = queryContext(graph, c.ref, {});
294
317
  for (const x of factsFromContext(ctx)) retrieved.add(x);
318
+ collectChecks(ctx);
295
319
  }
296
320
 
297
321
  const base = baselineContext(graph, s.touch);
@@ -303,8 +327,6 @@ export function runRoutingEval(root, { json = false } = {}) {
303
327
  const all = score(retrieved, new Set(s.truth));
304
328
  const baseAll = score(base, new Set(s.truth));
305
329
  const crit = score(critical, new Set(s.critical));
306
- const vf = score(verif, new Set(s.verification));
307
- const baseVf = score(baseV, new Set(s.verification));
308
330
  const noise = irrelevanceRatio(retrieved, closure);
309
331
  const baseNoise = irrelevanceRatio(base, baseClosure);
310
332
 
@@ -314,12 +336,14 @@ export function runRoutingEval(root, { json = false } = {}) {
314
336
  recall: all.hit / (all.total || 1),
315
337
  precision: 1 - noise,
316
338
  criticalRecall: crit.total ? crit.hit / crit.total : 1,
317
- verificationRecall: vf.total ? vf.hit / vf.total : 1,
339
+ verificationRecall: verificationRecall(verif, new Set(s.verification)),
340
+ checkSelectivity: checkSelectivity(graph, recommendedIds),
318
341
  noise,
319
342
  baseline: {
320
343
  recall: baseAll.hit / (baseAll.total || 1),
321
344
  noise: baseNoise,
322
- verificationRecall: baseVf.hit / (baseVf.total || 1),
345
+ verificationRecall: verificationRecall(baseV, new Set(s.verification)),
346
+ checkSelectivity: 1, // The baseline runs every verification entry and gate.
323
347
  },
324
348
  missed: all.missed.slice(0, 8),
325
349
  offClosure: [...retrieved].filter((r) => !closure.has(r)).slice(0, 8),
@@ -340,12 +364,14 @@ export function runRoutingEval(root, { json = false } = {}) {
340
364
  precision: round(mean("precision")),
341
365
  criticalRecall: round(mean("criticalRecall")),
342
366
  verificationRecall: round(mean("verificationRecall")),
367
+ checkSelectivity: round(mean("checkSelectivity")),
343
368
  noise: round(mean("noise")),
344
369
  retrievalReduction: 0,
345
370
  baseline: {
346
371
  recall: round(meanB("recall")),
347
372
  noise: round(meanB("noise")),
348
373
  verificationRecall: round(meanB("verificationRecall")),
374
+ checkSelectivity: round(meanB("checkSelectivity")),
349
375
  },
350
376
  };
351
377
  // Size proxy: retrieved fact count versus the whole-repository baseline.
@@ -5,5 +5,12 @@
5
5
  "touch": ["libs/protocol/ledger.pb.go"],
6
6
  "truth": ["component:default/protocol", "component:default/ingest", "component:default/gateway", "doc:libs/protocol/README.md"],
7
7
  "critical": [],
8
- "verification": []
8
+ "verification": [
9
+ "verify:go test ./...",
10
+ "verify:go vet ./...",
11
+ "verify-id:component:default/gateway:go-test",
12
+ "verify-id:component:default/gateway:go-vet",
13
+ "verify-id:component:default/ingest:go-test",
14
+ "verify-id:component:default/ingest:go-vet"
15
+ ]
9
16
  }
@@ -5,5 +5,11 @@
5
5
  "touch": ["shared/index.js"],
6
6
  "truth": ["component:default/shared", "component:default/edge", "component:default/console", "event:corp.telemetry.ingest.v1"],
7
7
  "critical": ["event:corp.telemetry.ingest.v1"],
8
- "verification": []
8
+ "verification": [
9
+ "verify:vitest run",
10
+ "verify:tsc --noEmit",
11
+ "verify-id:component:default/console:test",
12
+ "verify-id:component:default/console:typecheck",
13
+ "verify-id:component:default/edge:test"
14
+ ]
9
15
  }
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "e-01",
3
+ "repository": "fixture-e",
4
+ "title": "Change the shared engine crate",
5
+ "touch": ["crates/engine/src/lib.rs"],
6
+ "truth": ["component:default/engine", "component:default/cli"],
7
+ "critical": [],
8
+ "verification": [
9
+ "verify:cargo test -p engine",
10
+ "verify:cargo clippy -p engine",
11
+ "verify:cargo test -p cli",
12
+ "verify:cargo clippy -p cli"
13
+ ]
14
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "e-02",
3
+ "repository": "fixture-e",
4
+ "title": "Change the CLI crate manifest",
5
+ "touch": ["apps/cli/Cargo.toml"],
6
+ "truth": ["component:default/cli", "component:default/engine", "component:default/testutil", "external:serde"],
7
+ "critical": [],
8
+ "verification": ["verify:cargo test -p cli", "verify:cargo clippy -p cli"]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "e-03",
3
+ "repository": "fixture-e",
4
+ "title": "Change CLI code that looks like the logging crate and an ambiguous foo_bar import",
5
+ "touch": ["apps/cli/src/logging.rs", "apps/cli/src/main.rs"],
6
+ "truth": ["component:default/cli", "component:default/engine"],
7
+ "critical": [],
8
+ "verification": ["verify:cargo test -p cli", "verify:cargo clippy -p cli"]
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "id": "e-04",
3
+ "repository": "fixture-e",
4
+ "title": "Change each crate of the foo-bar / foo_bar pair",
5
+ "touch": ["crates/foo-bar/src/lib.rs", "crates/foo_bar/src/lib.rs"],
6
+ "truth": ["component:default/foo-bar", "component:default/foo-u-bar"],
7
+ "critical": [],
8
+ "verification": [
9
+ "verify:cargo test -p foo-bar",
10
+ "verify:cargo clippy -p foo-bar",
11
+ "verify:cargo test -p foo_bar",
12
+ "verify:cargo clippy -p foo_bar"
13
+ ]
14
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "f-01",
3
+ "repository": "fixture-f",
4
+ "title": "Change the shared ledger-core library",
5
+ "touch": ["libs/ledger-core/src/ledger_core/models.py"],
6
+ "truth": ["component:default/ledger-core", "component:default/ledger-api"],
7
+ "critical": [],
8
+ "verification": ["verify:uv run pytest", "verify:uv run ruff check", "verify:uv run mypy"]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "f-02",
3
+ "repository": "fixture-f",
4
+ "title": "Change the ledger API manifest",
5
+ "touch": ["services/ledger-api/pyproject.toml"],
6
+ "truth": ["component:default/ledger-api", "component:default/ledger-core", "component:default/ledger-testing", "external:sqlalchemy"],
7
+ "critical": [],
8
+ "verification": ["verify:uv run pytest", "verify:uv run ruff check", "verify:uv run mypy"]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "f-03",
3
+ "repository": "fixture-f",
4
+ "title": "Change the app that imports its own utils module",
5
+ "touch": ["services/b-app/b_app/main.py"],
6
+ "truth": ["component:default/b-app"],
7
+ "critical": [],
8
+ "verification": []
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "f-04",
3
+ "repository": "fixture-f",
4
+ "title": "Change a nested workspace member, not its parent",
5
+ "touch": ["python/packages/api/src/api_svc/main.py"],
6
+ "truth": ["component:default/api-svc", "component:default/core-lib"],
7
+ "critical": [],
8
+ "verification": []
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "f-05",
3
+ "repository": "fixture-f",
4
+ "title": "Change the ledger API Alembic migration",
5
+ "touch": ["services/ledger-api/alembic/versions/0001_init.py"],
6
+ "truth": ["component:default/ledger-api", "uses:resource:default/ledger-api-postgres"],
7
+ "critical": ["uses:resource:default/ledger-api-postgres"],
8
+ "verification": ["verify:uv run pytest", "verify:uv run ruff check", "verify:uv run mypy"]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "f-06",
3
+ "repository": "fixture-f",
4
+ "title": "Change the ledger job exercised by unittest",
5
+ "touch": ["jobs/ledger-jobs/tests/test_jobs.py"],
6
+ "truth": ["component:default/ledger-jobs"],
7
+ "critical": [],
8
+ "verification": ["verify:python -m unittest discover"]
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-doc-system",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Portable documentation and software-catalog system with an explicit authority model, for AI coding agents",
6
6
  "keywords": [
@@ -42,6 +42,7 @@
42
42
  "schemas",
43
43
  "skills",
44
44
  "templates",
45
+ "CHANGELOG.md",
45
46
  "INSTALL_FOR_AGENTS.md"
46
47
  ],
47
48
  "license": "MIT"
@@ -66,6 +66,46 @@
66
66
  }
67
67
  }
68
68
  },
69
+ "normativeCitation": {
70
+ "description": "One section of a configured normative document, pinned by the digest it had when it was reviewed. `agentdoc audit` lists every section with its current digest.",
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["path", "section", "sha256"],
74
+ "properties": {
75
+ "path": { "$ref": "#/$defs/repoPath" },
76
+ "section": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^\\S+$" },
77
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
78
+ }
79
+ },
80
+ "requirementKey": { "type": "string", "maxLength": 80, "pattern": "^requirement\\.[a-z0-9]+(?:[.-][a-z0-9]+)*$" },
81
+ "requirementValue": { "type": "string", "maxLength": 80, "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$" },
82
+ "requirement": {
83
+ "description": "An implementation obligation a normative document places on this component. It is an AUTHORED fact that must be corroborated by implementation evidence on the same key; it never stands on its own word.",
84
+ "type": "object",
85
+ "additionalProperties": false,
86
+ "required": ["id", "key", "value", "source"],
87
+ "properties": {
88
+ "id": { "$ref": "#/$defs/name" },
89
+ "key": { "$ref": "#/$defs/requirementKey" },
90
+ "value": { "$ref": "#/$defs/requirementValue" },
91
+ "source": { "$ref": "#/$defs/normativeCitation" },
92
+ "satisfiedBy": {
93
+ "description": "Adapters able to derive the implementation side of this requirement. Named so a missing extractor is reported as such instead of as silence.",
94
+ "type": "array", "minItems": 1, "maxItems": 12, "uniqueItems": true,
95
+ "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
96
+ },
97
+ "expects": {
98
+ "description": "What an adapter in satisfiedBy must find for the requirement to be provided: a host path installed, or a service managed. Without it an adapter has nothing deterministic to compare.",
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "oneOf": [{ "required": ["installs"] }, { "required": ["service"] }],
102
+ "properties": {
103
+ "installs": { "type": "string", "maxLength": 400, "pattern": "^/[^\\s{}]*[^\\s{}/]$" },
104
+ "service": { "type": "string", "maxLength": 200, "pattern": "^[A-Za-z0-9@._-]+$" }
105
+ }
106
+ }
107
+ }
108
+ },
69
109
  "warningAcceptance": {
70
110
  "type": "object",
71
111
  "additionalProperties": false,
@@ -27,7 +27,11 @@
27
27
  "description": "Published artifacts or remotes this component is consumed through.",
28
28
  "items": { "$ref": "common.json#/$defs/repoPath" }
29
29
  },
30
- "context": { "$ref": "common.json#/$defs/context" }
30
+ "context": { "$ref": "common.json#/$defs/context" },
31
+ "requirements": {
32
+ "type": "array", "minItems": 1, "maxItems": 64,
33
+ "items": { "$ref": "common.json#/$defs/requirement" }
34
+ }
31
35
  },
32
36
  "oneOf": [
33
37
  { "required": ["system"], "not": { "anyOf": [{ "required": ["domain"] }, { "required": ["placementRationale"] }] } },
@@ -358,7 +358,8 @@
358
358
  "capability",
359
359
  "component-placement",
360
360
  "event-producer",
361
- "event-consumer"
361
+ "event-consumer",
362
+ "requirement-satisfaction"
362
363
  ]
363
364
  },
364
365
  "subject": {
@@ -416,6 +417,12 @@
416
417
  "minLength": 1,
417
418
  "maxLength": 200
418
419
  },
420
+ "key": {
421
+ "$ref": "common.json#/$defs/requirementKey"
422
+ },
423
+ "value": {
424
+ "$ref": "common.json#/$defs/requirementValue"
425
+ },
419
426
  "reason": {
420
427
  "$ref": "common.json#/$defs/rationale"
421
428
  },
@@ -633,6 +640,59 @@
633
640
  ]
634
641
  }
635
642
  }
643
+ },
644
+ {
645
+ "if": {
646
+ "properties": {
647
+ "fact": {
648
+ "const": "requirement-satisfaction"
649
+ }
650
+ },
651
+ "required": [
652
+ "fact"
653
+ ]
654
+ },
655
+ "then": {
656
+ "required": [
657
+ "key",
658
+ "value"
659
+ ],
660
+ "not": {
661
+ "anyOf": [
662
+ {
663
+ "required": [
664
+ "target"
665
+ ]
666
+ },
667
+ {
668
+ "required": [
669
+ "transport"
670
+ ]
671
+ },
672
+ {
673
+ "required": [
674
+ "role"
675
+ ]
676
+ }
677
+ ]
678
+ }
679
+ },
680
+ "else": {
681
+ "not": {
682
+ "anyOf": [
683
+ {
684
+ "required": [
685
+ "key"
686
+ ]
687
+ },
688
+ {
689
+ "required": [
690
+ "value"
691
+ ]
692
+ }
693
+ ]
694
+ }
695
+ }
636
696
  }
637
697
  ]
638
698
  }
@@ -644,6 +704,51 @@
644
704
  "$ref": "common.json#/$defs/warningAcceptance"
645
705
  }
646
706
  },
707
+ "normativeCoverage": {
708
+ "description": "Reviewed attestations that a normative section carries no implementation obligation. Pinned by the section digest; a changed section invalidates its attestation and forces a fresh review.",
709
+ "type": "array",
710
+ "maxItems": 2048,
711
+ "items": {
712
+ "type": "object",
713
+ "additionalProperties": false,
714
+ "required": [
715
+ "path",
716
+ "section",
717
+ "sha256",
718
+ "disposition",
719
+ "reason",
720
+ "reviewWhen"
721
+ ],
722
+ "properties": {
723
+ "path": {
724
+ "$ref": "common.json#/$defs/repoPath"
725
+ },
726
+ "section": {
727
+ "type": "string",
728
+ "minLength": 1,
729
+ "maxLength": 200,
730
+ "pattern": "^\\S+$"
731
+ },
732
+ "sha256": {
733
+ "type": "string",
734
+ "pattern": "^[0-9a-f]{64}$"
735
+ },
736
+ "disposition": {
737
+ "enum": [
738
+ "no-implementation-obligation",
739
+ "informative"
740
+ ]
741
+ },
742
+ "reason": {
743
+ "$ref": "common.json#/$defs/rationale"
744
+ },
745
+ "reviewWhen": {
746
+ "type": "string",
747
+ "minLength": 12
748
+ }
749
+ }
750
+ }
751
+ },
647
752
  "output": {
648
753
  "type": "object",
649
754
  "additionalProperties": false,
@@ -678,6 +783,17 @@
678
783
  },
679
784
  "runbookDir": {
680
785
  "$ref": "common.json#/$defs/repoPath"
786
+ },
787
+ "normative": {
788
+ "description": "Globs of normative documents (specifications, technical profiles). Every level-2 section of a matched Markdown document must be cited by a component requirement or attested in normativeCoverage; otherwise AGENTDOC_NORMATIVE_UNCOVERED.",
789
+ "type": "array",
790
+ "minItems": 1,
791
+ "maxItems": 64,
792
+ "uniqueItems": true,
793
+ "items": {
794
+ "type": "string",
795
+ "minLength": 1
796
+ }
681
797
  }
682
798
  }
683
799
  }
@@ -96,7 +96,11 @@ not work around it: run `agentdoc compile`, review the diff, commit the graph.
96
96
  - **Never hand-edit `.agentdoc/graph.json`.** The gate rejects it, and it would
97
97
  be wrong anyway.
98
98
  - **Do not trust existing documentation.** In MIGRATE it is evidence of what
99
- someone once believed; the audit says where that belief contradicts the code.
99
+ someone once believed. The audit flags only a narrow set of contradictions
100
+ automatically (negated components, port claims); it does not read prose for
101
+ meaning. Obligations in specifications are checked only once declared:
102
+ configure `docs.normative`, turn every section into a component requirement
103
+ or a reviewed attestation, and finish with `agentdoc check --strict`.
100
104
 
101
105
  ## Deeper references
102
106
 
@@ -5,13 +5,18 @@ operates on a catalog can be run from any subdirectory of the repository: the
5
5
  configuration is found by walking up to `agentdoc/agentdoc.config.yaml`. No
6
6
  command ever needs the agentdoc source tree or a path to it.
7
7
 
8
- Exit codes: `0` ok, `1` failure, `2` usage.
8
+ Exit codes: `0` ok, `1` failure, `2` usage. Flags are declared per command:
9
+ an unknown or misspelled flag is a usage error (`2`), with a suggestion when
10
+ one is close — it is never silently ignored, so `check --strcit` cannot pass
11
+ for `check --strict`. `agentdoc <command> --help` lists exactly what a command
12
+ accepts.
9
13
 
10
14
  ## Meta
11
15
 
12
16
  ```bash
13
17
  agentdoc --version # installed version
14
- agentdoc --help # command list (also: agentdoc <command> --help)
18
+ agentdoc --help # command list
19
+ agentdoc <command> --help # every flag of one command (also: agentdoc help <command>)
15
20
  agentdoc doctor # machine check: is this CLI install healthy
16
21
  agentdoc doctor --project # the same, plus the current project's catalog health
17
22
  ```
@@ -42,13 +47,16 @@ failures as part of its output.
42
47
  ```bash
43
48
  agentdoc validate # sources + schemas + semantic checks
44
49
  agentdoc compile [--require-clean] # deterministic graph build, atomic write
45
- agentdoc check [--require-clean] [--allow-stale-observations]
50
+ agentdoc check [--strict] [--require-clean] [--allow-stale-observations]
46
51
  ```
47
52
 
48
53
  `compile` writes `.agentdoc/graph.json` (or the path in `output.graph`).
49
54
  `check` is the gate: it fails on a stale or hand-edited graph, a compiler or
50
55
  schema version mismatch, an observation past its declared `maxAgeDays`, and —
51
- with `--require-clean` — catalog inputs that differ from `HEAD`.
56
+ with `--require-clean` — catalog inputs that differ from `HEAD`. With
57
+ `--strict` it also fails on every warning not accepted in `warningAcceptances`
58
+ — the gate that ends a MIGRATE (unreviewed normative sections, unproven
59
+ requirements, orphan documents).
52
60
 
53
61
  ## Query and impact
54
62
 
@@ -40,9 +40,19 @@ never copies the agentdoc implementation into the repository.
40
40
  In `agentdoc.config.yaml`:
41
41
 
42
42
  - Set `namespace`, fix `discovery.componentDescriptors` to match the real
43
- layout, and enable the adapters for the technologies present. `adapters: []`
44
- is a valid but useless starting point: with no adapter, a component
45
- descriptor is an unsupported claim and compilation fails on purpose.
43
+ layout, and enable the adapters for the technologies present. `init`
44
+ pre-selects the language and deployment adapters whose manifests it finds:
45
+ `node`, `go`, `rust` and `python`, plus `dockerfile` and `wrangler`. It also
46
+ adds `database`, `openapi`, `events` and `github-actions`. Add the rest
47
+ (`pnpm`, `turborepo`, `typescript`, `protobuf`, `graphql`, `ansible`, …) when
48
+ the repository uses them. `adapters: []` is a valid but useless starting
49
+ point: with no adapter, a component descriptor is an unsupported claim and
50
+ compilation fails on purpose.
51
+ - An existing catalog does not gain new adapters on upgrade. After upgrading
52
+ the CLI, compare `adapters:` with the technologies present, for example a
53
+ repository with `Cargo.toml` or `pyproject.toml` units and no `rust` /
54
+ `python` entry. Then run `agentdoc audit`: newly visible units appear as
55
+ `AGENTDOC_COVERAGE_ZERO` until they have a descriptor.
46
56
  - Three example authority rules are written **commented out**. Uncomment only
47
57
  the ones you can justify, with the rationale in your own words. An authority
48
58
  rule is a governance decision; the scaffolder does not make it for you.
@@ -81,8 +91,14 @@ agentdoc audit > migration-report.json
81
91
 
82
92
  Then, in order:
83
93
 
84
- 1. **Source-of-truth mapping.** For each major claim, record which inventory
85
- supports it. Anything supported by none goes to `reviewedAmbiguity`.
94
+ 1. **Source-of-truth mapping.** Set `docs.normative` to the specifications and
95
+ profiles, then empty `normative.uncovered`: each section becomes a
96
+ `spec.requirements` entry on the component that must fulfil it, or a
97
+ `normativeCoverage` attestation that it obliges nothing. Prove each
98
+ requirement (adapter evidence or a `requirement-satisfaction` override);
99
+ an unproven one stays `REQUIREMENT_UNVERIFIED`. For other major claims,
100
+ record which inventory supports them; anything supported by none goes to
101
+ `reviewedAmbiguity`.
86
102
  2. **Contradictions.** Work `contradictions` and `documentationContradictions`.
87
103
  Each needs a human decision: fix the docs, fix the code, or add an authority
88
104
  rule saying which class wins for that key.
@@ -91,7 +107,13 @@ Then, in order:
91
107
  5. **Stale contracts.** Point the API descriptor at the current contract file.
92
108
  6. **Ownership.** Placement from code where unambiguous, from a human elsewhere.
93
109
  7. **Catalog.** `scaffold --write`, review every generated file.
94
- 8. **Gate.** `validate` → `compile` → `check`, then install CI.
110
+ 8. **Gate.** `validate` → `compile` → `check --strict`, then install CI. Strict
111
+ fails on any unaccepted warning: an unreviewed section or an unproven
112
+ requirement cannot ship in a "complete" migration.
113
+
114
+ **The audit does not read prose for meaning.** It checks only negated
115
+ components and port claims automatically; every other documented obligation is
116
+ caught only if it is declared as a requirement.
95
117
 
96
118
  **Do not trust existing documentation.** The single most common migration
97
119
  failure is copying existing prose into descriptions. If you cannot write a