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,426 @@
1
+ // Agent-routing evaluation.
2
+ //
3
+ // The question this answers is not "does the catalog compile" but "does routing
4
+ // an agent through it retrieve the right context, and less noise than unassisted
5
+ // repository exploration".
6
+ //
7
+ // Two retrieval systems are measured on the same scenarios:
8
+ //
9
+ // baseline what an agent gets by exploring the repository the obvious way:
10
+ // every documentation file, every descriptor, every source file
11
+ // under the touched component. This is the honest shape of "just
12
+ // read the repo", and it is what the catalog has to beat.
13
+ //
14
+ // catalog what `agentdoc query` returns for the same scenario.
15
+ //
16
+ // Retrieval-set overlap against a per-scenario ground truth gives precision,
17
+ // recall, critical-constraint recall, verification-command recall and noise.
18
+ // No model is involved, so the measurement is deterministic and cannot be
19
+ // gamed by phrasing.
20
+ //
21
+ // THRESHOLDS ARE PRE-REGISTERED in thresholds.json. They were fixed before the
22
+ // scenarios were run and must not be moved to make a run pass.
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { compile } from "../core/compile.mjs";
27
+ import { AgentDocError, CODES } from "../core/codes.mjs";
28
+ import { queryContext, ContextIndex } from "../core/query.mjs";
29
+ import { impact } from "../core/impact.mjs";
30
+
31
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
32
+
33
+ // A retrieved item is a "fact reference" the agent would have to understand:
34
+ // an entity ref, a constraint path, a verification command, a contract ref.
35
+ export function factsFromContext(ctx) {
36
+ const out = new Set();
37
+ out.add(ctx.entity.ref);
38
+ for (const r of ctx.relations.items) out.add(r.type + ":" + r.other);
39
+ for (const p of ctx.journeyPeers || []) out.add(p);
40
+ for (const s2 of ctx.placement.systems) out.add(s2);
41
+ for (const d2 of ctx.placement.domains) out.add(d2);
42
+ for (const a of ctx.apis.provided) out.add("provides:" + a.api);
43
+ for (const a of ctx.apis.consumed) out.add("consumes:" + a.api);
44
+ for (const c of ctx.apis.consumers || []) out.add("consumer:" + c);
45
+ if (ctx.apis.provider) out.add("provider:" + ctx.apis.provider);
46
+ for (const r of ctx.resources.used) out.add("uses:" + r);
47
+ for (const r of ctx.resources.usedBy) out.add("usedBy:" + r);
48
+ for (const e of ctx.events.items) out.add("event:" + e.pattern);
49
+ for (const e of ctx.externalDependencies.items) out.add("external:" + e.name);
50
+ for (const v of ctx.verification.items) out.add("verify:" + v.command);
51
+ for (const p of ctx.constraints.constraints) out.add("constraint:" + p);
52
+ for (const d of ctx.constraints.docs) out.add("doc:" + d);
53
+ for (const r of ctx.constraints.runbooks) out.add("runbook:" + r);
54
+ for (const c of ctx.authority.conflicts.items) out.add("conflict:" + c.key);
55
+ for (const o of ctx.authority.observations) out.add("observation:" + o.id);
56
+ return out;
57
+ }
58
+
59
+ // Critical context is what an agent gets wrong with the most damage if it is
60
+ // missing: a constraint, a live conflict, a runtime observation, a data
61
+ // resource, an API boundary, or an event subject it publishes or consumes.
62
+ export function criticalFromContext(ctx) {
63
+ const out = new Set();
64
+ for (const e of ctx.events.items) out.add("event:" + e.pattern);
65
+ for (const p of ctx.constraints.constraints) out.add("constraint:" + p);
66
+ for (const c of ctx.authority.conflicts.items) out.add("conflict:" + c.key);
67
+ for (const o of ctx.authority.observations) out.add("observation:" + o.id);
68
+ for (const r of ctx.resources.used) out.add("uses:" + r);
69
+ for (const r of ctx.resources.usedBy) out.add("usedBy:" + r);
70
+ for (const a of ctx.apis.consumed) out.add("consumes:" + a.api);
71
+ for (const s2 of ctx.placement.systems) out.add(s2);
72
+ for (const d2 of ctx.placement.domains) out.add(d2);
73
+ for (const a of ctx.apis.provided) out.add("provides:" + a.api);
74
+ for (const c of ctx.apis.consumers || []) out.add("consumer:" + c);
75
+ return out;
76
+ }
77
+
78
+ export function verificationFromContext(ctx) {
79
+ return new Set(ctx.verification.items.map((v) => "verify:" + v.command));
80
+ }
81
+
82
+ // ── baseline: unassisted repository exploration ────────────────────────
83
+ export function baselineContext(graph, touch) {
84
+ const idx = new ContextIndex(graph);
85
+ const seeds = touch.map((t) => idx.resolve(t)).filter(Boolean);
86
+ const roots = seeds.map((e) => e.derived?.sourcePath).filter(Boolean);
87
+ const out = new Set();
88
+ for (const e of graph.entities) {
89
+ const d = e.derived || {};
90
+ const inSeed = seeds.includes(e);
91
+ const underRoot = roots.some((r) => e.ref && r && d.sourcePath && (d.sourcePath === r || d.sourcePath.startsWith(r + "/")));
92
+ if (!inSeed && !underRoot) continue;
93
+ out.add(e.ref);
94
+ for (const r of graph.relations) {
95
+ if (r.sourceRef === e.ref) out.add(r.type + ":" + r.targetRef);
96
+ if (r.targetRef === e.ref) out.add(r.type + ":" + r.sourceRef);
97
+ }
98
+ for (const ev of graph.interfaces.events) {
99
+ if (ev.producers.includes(e.ref) || ev.consumers.includes(e.ref)) out.add("event:" + ev.pattern);
100
+ }
101
+ for (const x of graph.externalDependencies) if (x.componentRef === e.ref) out.add("external:" + x.name);
102
+ for (const v of graph.verification) if (v.componentRefs.includes(e.ref)) out.add("verify:" + v.command);
103
+ for (const c of graph.conflicts) if (c.subject === e.ref) out.add("conflict:" + c.key);
104
+ for (const o of graph.observations) if (o.facts.some((f) => f.subject === e.ref)) out.add("observation:" + o.id);
105
+ for (const p of d.docs || []) out.add("doc:" + p);
106
+ for (const p of d.constraints || []) out.add("constraint:" + p);
107
+ for (const p of d.runbooks || []) out.add("runbook:" + p);
108
+ for (const r of graph.relations) {
109
+ if (r.sourceRef === e.ref || r.targetRef === e.ref) out.add(r.type + ":" + (r.sourceRef === e.ref ? r.targetRef : r.sourceRef));
110
+ }
111
+ }
112
+ // A repository-wide documentation sweep is what an agent does when the
113
+ // routing is not available: read everything that looks like documentation.
114
+ for (const f of new Set([...repoDocPaths(graph)])) out.add("repodoc:" + f);
115
+ return out;
116
+ }
117
+
118
+ function repoDocPaths(graph) {
119
+ const out = new Set();
120
+ for (const e of graph.entities) {
121
+ for (const p of (e.derived || {}).docs || []) out.add(p);
122
+ }
123
+ for (const cfgp of graph.verification) for (const p of cfgp.configPaths || []) out.add(p);
124
+ return [...out].sort();
125
+ }
126
+
127
+ // Baseline verification recall: without routing, an agent runs the union of
128
+ // every check in the repository (or nothing). The realistic reading of
129
+ // "unassisted" is the union, since no routing exists to narrow it.
130
+ // The whole-repository baseline: what an agent that has no routing ends up
131
+ // holding. Every entity, every relation, every subject, every check, every
132
+ // document. This is the honest cost of "just read the repository".
133
+ export function fullRepositoryContext(graph) {
134
+ const out = new Set();
135
+ for (const e of graph.entities) {
136
+ out.add(e.ref);
137
+ const d = e.derived || {};
138
+ for (const p of d.docs || []) out.add("doc:" + p);
139
+ for (const p of d.constraints || []) out.add("constraint:" + p);
140
+ for (const p of d.runbooks || []) out.add("runbook:" + p);
141
+ }
142
+ for (const r of graph.relations) out.add(r.type + ":" + (r.sourceRef === undefined ? "" : r.targetRef));
143
+ for (const r of graph.relations) out.add(r.type + ":" + r.sourceRef);
144
+ for (const ev of graph.interfaces.events) out.add("event:" + ev.pattern);
145
+ for (const x of graph.externalDependencies) out.add("external:" + x.name);
146
+ for (const x of graph.capabilities) out.add("capability:" + x.name);
147
+ for (const v of graph.verification) out.add("verify:" + v.command);
148
+ for (const c of graph.conflicts) out.add("conflict:" + c.key);
149
+ for (const o of graph.observations) out.add("observation:" + o.id);
150
+ for (const p of repoDocPaths(graph)) out.add("repodoc:" + p);
151
+ return out;
152
+ }
153
+
154
+ // Baseline verification: without routing, an agent runs the union of every
155
+ // check in the repository, because nothing narrows it.
156
+ function baselineVerification(graph) {
157
+ return new Set(graph.verification.map((v) => "verify:" + v.command));
158
+ }
159
+
160
+ function score(retrieved, truth) {
161
+ let hit = 0;
162
+ const missed = [];
163
+ for (const t of truth) {
164
+ if (retrieved.has(t)) hit++;
165
+ else missed.push(t);
166
+ }
167
+ return { hit, total: truth.size, missed, precision: truth.size ? hit / truth.size : 1 };
168
+ }
169
+
170
+ function noiseRatio(retrieved, truth) {
171
+ if (!retrieved.size) return 0;
172
+ let n = 0;
173
+ for (const r of retrieved) if (!truth.has(r)) n++;
174
+ return n / retrieved.size;
175
+ }
176
+
177
+ // ── Irrelevance, measured against reachability rather than against the
178
+ // ground truth.
179
+ //
180
+ // The first definition of "noise" was the complement of truth recall. It is
181
+ // degenerate: a scenario lists the handful of facts that matter, so any
182
+ // additional legitimate fact — a sibling constraint, a second check, an
183
+ // adjacent contract — is scored as noise, and the metric rewards returning
184
+ // almost nothing. It was replaced, before any fixture other than Koda was
185
+ // measured, with a question that does not depend on the truth set at all:
186
+ //
187
+ // did routing drag in anything the graph itself says is unrelated?
188
+ //
189
+ // Relevance is the one-hop graph closure of the touched entities, computed from
190
+ // the compiled graph, so the metric cannot be satisfied by inflating a truth
191
+ // set. Recall is still measured against the truth sets, unchanged.
192
+ export function relevantClosure(graph, touch) {
193
+ const idx = new ContextIndex(graph);
194
+ const seeds = [];
195
+ for (const t of touch) {
196
+ const e = idx.resolve(t);
197
+ if (e) seeds.push(e);
198
+ }
199
+ // Exactly one hop, as documented. A transitive closure grows to most of the
200
+ // repository, and then a router that returns everything scores as precise:
201
+ // the metric would stop measuring what it claims to measure.
202
+ const seeds2 = new Set(seeds.map((e) => e.ref));
203
+ const refs = new Set(seeds2);
204
+ for (const r of graph.relations) {
205
+ if (seeds2.has(r.sourceRef)) refs.add(r.targetRef);
206
+ if (seeds2.has(r.targetRef)) refs.add(r.sourceRef);
207
+ }
208
+ const out = new Set();
209
+ for (const ref of refs) out.add(ref);
210
+ for (const r of graph.relations) {
211
+ if (refs.has(r.sourceRef)) out.add(r.type + ":" + r.targetRef);
212
+ if (refs.has(r.targetRef)) out.add(r.type + ":" + r.sourceRef);
213
+ }
214
+ for (const e of graph.entities) {
215
+ if (!refs.has(e.ref)) continue;
216
+ const d = e.derived || {};
217
+ for (const p of d.docs || []) out.add("doc:" + p);
218
+ for (const p of d.constraints || []) out.add("constraint:" + p);
219
+ for (const p of d.runbooks || []) out.add("runbook:" + p);
220
+ for (const ev of graph.interfaces.events) {
221
+ if (ev.producers.includes(e.ref) || ev.consumers.includes(e.ref) || (d.sourcePath && ev.contract.ref.startsWith(d.sourcePath + "/"))) {
222
+ out.add("event:" + ev.pattern);
223
+ }
224
+ }
225
+ for (const x of graph.externalDependencies) if (x.componentRef === e.ref) out.add("external:" + x.name);
226
+ for (const x of graph.capabilities) if (x.componentRef === e.ref) out.add("capability:" + x.name);
227
+ for (const v of graph.verification) if (v.componentRefs.includes(e.ref)) out.add("verify:" + v.command);
228
+ for (const c of graph.conflicts) if (c.subject === e.ref) out.add("conflict:" + c.key);
229
+ for (const o of graph.observations) if (o.facts.some((f) => f.subject === e.ref)) out.add("observation:" + o.id);
230
+ for (const r of graph.relations) {
231
+ if (r.type === "usesResource" && r.sourceRef === e.ref) out.add("uses:" + r.targetRef);
232
+ if (r.type === "usesResource" && r.targetRef === e.ref) out.add("usedBy:" + r.sourceRef);
233
+ if (r.type === "consumesApi" && r.sourceRef === e.ref) out.add("consumes:" + r.targetRef);
234
+ if (r.type === "consumesApi" && r.targetRef === e.ref) out.add("consumer:" + r.sourceRef);
235
+ if (r.type === "providesApi" && r.sourceRef === e.ref) out.add("provides:" + r.targetRef);
236
+ if (r.type === "providesApi" && r.targetRef === e.ref) out.add("provider:" + r.sourceRef);
237
+ }
238
+ }
239
+ return out;
240
+ }
241
+
242
+ function irrelevanceRatio(retrieved, closure) {
243
+ if (!retrieved.size) return 0;
244
+ let n = 0;
245
+ for (const r of retrieved) if (!closure.has(r)) n++;
246
+ return n / retrieved.size;
247
+ }
248
+
249
+ export function runRoutingEval(root, { json = false } = {}) {
250
+ void json;
251
+ const thresholds = JSON.parse(fs.readFileSync(path.join(HERE, "thresholds.json"), "utf8"));
252
+ const scenarioDir = path.join(HERE, "scenarios");
253
+ const files = fs.readdirSync(scenarioDir).filter((f) => f.endsWith(".json")).sort();
254
+ const res = compile(root);
255
+ if (res.errors.length) {
256
+ throw new AgentDocError(
257
+ CODES.CONFIG,
258
+ "the routing evaluation requires a compiling repository: " + res.errors.map((e) => e.code + " " + e.message).join("; ")
259
+ );
260
+ }
261
+ const graph = res.graph;
262
+ const tag = fixtureTag(root);
263
+
264
+ const perScenario = [];
265
+ for (const f of files) {
266
+ const s = JSON.parse(fs.readFileSync(path.join(scenarioDir, f), "utf8"));
267
+ // Scenarios are repository-scoped so a fixture's expectations can never
268
+ // leak into another repository's score.
269
+ if ((s.repository || null) !== tag) continue;
270
+
271
+ const retrieved = new Set();
272
+ const critical = new Set();
273
+ const verif = new Set();
274
+ let unresolved = 0;
275
+ for (const t of s.touch) {
276
+ let ctx;
277
+ try {
278
+ ctx = queryContext(graph, t, {});
279
+ } catch {
280
+ // A touched path that no entity claims is a legitimate answer, not a
281
+ // failure; the impact pass below is what routes it.
282
+ unresolved++;
283
+ continue;
284
+ }
285
+ for (const x of factsFromContext(ctx)) retrieved.add(x);
286
+ for (const x of criticalFromContext(ctx)) critical.add(x);
287
+ for (const x of verificationFromContext(ctx)) verif.add(x);
288
+ }
289
+ // Change impact is part of routing: it is what tells the agent which other
290
+ // components a change reaches. Its output is legitimately part of context.
291
+ const im = impact(graph, s.touch);
292
+ for (const c of im.affectedComponents) {
293
+ const ctx = queryContext(graph, c.ref, {});
294
+ for (const x of factsFromContext(ctx)) retrieved.add(x);
295
+ }
296
+
297
+ const base = baselineContext(graph, s.touch);
298
+ const baseV = baselineVerification(graph);
299
+ const full = fullRepositoryContext(graph);
300
+
301
+ const closure = relevantClosure(graph, s.touch);
302
+ const baseClosure = relevantClosure(graph, s.touch);
303
+ const all = score(retrieved, new Set(s.truth));
304
+ const baseAll = score(base, new Set(s.truth));
305
+ 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
+ const noise = irrelevanceRatio(retrieved, closure);
309
+ const baseNoise = irrelevanceRatio(base, baseClosure);
310
+
311
+ perScenario.push({
312
+ id: s.id,
313
+ title: s.title,
314
+ recall: all.hit / (all.total || 1),
315
+ precision: 1 - noise,
316
+ criticalRecall: crit.total ? crit.hit / crit.total : 1,
317
+ verificationRecall: vf.total ? vf.hit / vf.total : 1,
318
+ noise,
319
+ baseline: {
320
+ recall: baseAll.hit / (baseAll.total || 1),
321
+ noise: baseNoise,
322
+ verificationRecall: baseVf.hit / (baseVf.total || 1),
323
+ },
324
+ missed: all.missed.slice(0, 8),
325
+ offClosure: [...retrieved].filter((r) => !closure.has(r)).slice(0, 8),
326
+ unresolvedTouches: unresolved,
327
+ retrievedCount: retrieved.size,
328
+ baselineCount: base.size,
329
+ fullBaselineCount: full.size,
330
+ fullRecall: (() => { const sc = score(full, new Set(s.truth)); return sc.total ? sc.hit / sc.total : 0; })(),
331
+ });
332
+ }
333
+
334
+ const mean = (k) => (perScenario.length ? perScenario.reduce((a, b) => a + b[k], 0) / perScenario.length : 0);
335
+ const meanB = (k) => (perScenario.length ? perScenario.reduce((a, b) => a + b.baseline[k], 0) / perScenario.length : 0);
336
+ void meanB;
337
+ const metrics = {
338
+ scenarios: perScenario.length,
339
+ recall: round(mean("recall")),
340
+ precision: round(mean("precision")),
341
+ criticalRecall: round(mean("criticalRecall")),
342
+ verificationRecall: round(mean("verificationRecall")),
343
+ noise: round(mean("noise")),
344
+ retrievalReduction: 0,
345
+ baseline: {
346
+ recall: round(meanB("recall")),
347
+ noise: round(meanB("noise")),
348
+ verificationRecall: round(meanB("verificationRecall")),
349
+ },
350
+ };
351
+ // Size proxy: retrieved fact count versus the whole-repository baseline.
352
+ const catCount = perScenario.reduce((a, b) => a + b.retrievedCount, 0);
353
+
354
+ const fullCount = perScenario.reduce((a, b) => a + b.fullBaselineCount, 0);
355
+ const fullRecall = perScenario.length
356
+ ? perScenario.reduce((a, b) => a + b.fullRecall, 0) / perScenario.length
357
+ : 0;
358
+ metrics.fullBaseline = { size: Math.round(fullCount / (perScenario.length || 1)), recall: round(fullRecall) };
359
+ metrics.retrievalReduction = fullCount ? round(1 - catCount / fullCount) : 0;
360
+
361
+ // The size-reduction metric is only meaningful once the repository is big
362
+ // enough for "the whole repository" to differ from "one component". Below the
363
+ // floor it is reported as not measured rather than failed: a single-component
364
+ // repository has almost nothing to route away from, and failing it would be
365
+ // punishing the fixture for being small. The aggregate gate in run-all.mjs is
366
+ // where the reduction threshold is enforced.
367
+ const SIZE_FLOOR = 100;
368
+ const sizeMeasured = (metrics.fullBaseline.size || 0) >= SIZE_FLOOR;
369
+
370
+ // Each check is (name, actual, bound, direction). Thresholds are the ones
371
+ // pre-registered in thresholds.json; none of them was moved to make a run
372
+ // pass.
373
+ const checks = [
374
+ ["recall", metrics.recall, thresholds.minRecall, "min"],
375
+ ["criticalRecall", metrics.criticalRecall, thresholds.minCriticalRecall, "min"],
376
+ ["verificationRecall", metrics.verificationRecall, thresholds.minVerificationRecall, "min"],
377
+ ["precision", metrics.precision, thresholds.minPrecision, "min"],
378
+ ["noise", metrics.noise, thresholds.maxNoise, "max"],
379
+ ["recallNotWorseThanTargetedBaseline", metrics.recall - metrics.baseline.recall, 0, "min"],
380
+ ["recallVsFullRepositoryBaseline", metrics.recall / (metrics.fullBaseline.recall || 1), thresholds.minRecallOverBaseline, "min"],
381
+ ];
382
+ if (sizeMeasured) checks.push(["retrievalReduction", metrics.retrievalReduction, thresholds.minRetrievalReduction, "min"]);
383
+ const failures = [];
384
+ for (const [name, actual, bound, dir] of checks) {
385
+ const ok = dir === "max" ? actual <= bound : actual >= bound;
386
+ if (!ok) failures.push(name + "=" + actual + " violates " + (dir === "max" ? "<=" : ">=") + bound);
387
+ }
388
+ // A repository with no scenarios has nothing to measure. Failing it would make
389
+ // every fresh install's CI red for a reason that has nothing to do with the
390
+ // system, and "recall=0" is a misleading way to say so.
391
+ if (!perScenario.length) {
392
+ return {
393
+ metrics: null,
394
+ thresholds,
395
+ notMeasured: ["no routing scenarios are registered for this repository"],
396
+ perScenario: [],
397
+ failures: [],
398
+ verdict: "SKIP",
399
+ };
400
+ }
401
+
402
+ return {
403
+ metrics,
404
+ thresholds,
405
+ notMeasured: sizeMeasured ? [] : ["retrievalReduction (full baseline below " + SIZE_FLOOR + " facts)"],
406
+ perScenario,
407
+ failures,
408
+ verdict: failures.length ? "FAIL" : "PASS",
409
+ };
410
+ }
411
+
412
+ function round(x) {
413
+ return Math.round(x * 10000) / 10000;
414
+ }
415
+
416
+ // A fixture declares which scenarios belong to it. The real reference
417
+ // repository has no tag, so only untagged scenarios run against it.
418
+ export function fixtureTag(root) {
419
+ try {
420
+ return JSON.parse(fs.readFileSync(path.join(root, ".agentdoc-fixture.json"), "utf8")).tag ?? null;
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+
426
+ export { compile };
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "a-01",
3
+ "repository": "fixture-a",
4
+ "title": "Change the orders API contract",
5
+ "touch": ["contracts/orders.openapi.yaml"],
6
+ "truth": ["api:default/orders-api", "component:default/orders-service", "provides:api:default/orders-api", "provider:component:default/orders-service"],
7
+ "critical": ["provides:api:default/orders-api"],
8
+ "verification": ["verify:pnpm --filter @orders/service test", "verify:pnpm --filter @orders/service typecheck"]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "a-02",
3
+ "repository": "fixture-a",
4
+ "title": "Change the orders database schema",
5
+ "touch": ["service/db/migrations/0001_orders.sql"],
6
+ "truth": ["component:default/orders-service", "uses:resource:default/orders-service-postgres", "constraint:service/CONSTRAINTS.md"],
7
+ "critical": ["uses:resource:default/orders-service-postgres", "constraint:service/CONSTRAINTS.md"],
8
+ "verification": ["verify:pnpm --filter @orders/service test", "verify:vitest run"]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "a-03",
3
+ "repository": "fixture-a",
4
+ "title": "Change order write behaviour and prove it still obeys the money rule",
5
+ "touch": ["service/src/index.ts"],
6
+ "truth": ["component:default/orders-service", "constraint:service/CONSTRAINTS.md", "doc:service/README.md"],
7
+ "critical": ["constraint:service/CONSTRAINTS.md"],
8
+ "verification": ["verify:pnpm --filter @orders/service test", "verify:vitest run", "verify:tsc --noEmit"]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "b-01",
3
+ "repository": "fixture-b",
4
+ "title": "Change the ledger protobuf contract",
5
+ "touch": ["proto/ledger.proto"],
6
+ "truth": ["api:default/ledger-service", "component:default/ingest", "component:default/protocol", "provides:api:default/ledger-service"],
7
+ "critical": ["provides:api:default/ledger-service"],
8
+ "verification": ["verify:go test ./...", "verify:go vet ./..."]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "b-02",
3
+ "repository": "fixture-b",
4
+ "title": "Change the only-writer rule for the ledger",
5
+ "touch": ["services/ingest/internal/relay/relay.go"],
6
+ "truth": ["component:default/ingest", "system:default/ledger", "provides:api:default/ledger-service"],
7
+ "critical": ["provides:api:default/ledger-service"],
8
+ "verification": ["verify:go test ./...", "verify:go vet ./..."]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "b-03",
3
+ "repository": "fixture-b",
4
+ "title": "Change the generated wire bindings",
5
+ "touch": ["libs/protocol/ledger.pb.go"],
6
+ "truth": ["component:default/protocol", "component:default/ingest", "component:default/gateway", "doc:libs/protocol/README.md"],
7
+ "critical": [],
8
+ "verification": []
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "b-04",
3
+ "repository": "fixture-b",
4
+ "title": "Break the append-to-read journey",
5
+ "touch": ["services/gateway/cmd/gateway/main.go"],
6
+ "truth": ["component:default/gateway", "component:default/ingest", "uses:resource:default/gateway-postgres", "constraint:services/gateway/CONSTRAINTS.md"],
7
+ "critical": ["uses:resource:default/gateway-postgres", "constraint:services/gateway/CONSTRAINTS.md"],
8
+ "verification": ["verify:go test ./...", "verify:go vet ./..."]
9
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "c-01",
3
+ "repository": "fixture-c",
4
+ "title": "Change the edge worker's flush schedule",
5
+ "touch": [
6
+ "edge/wrangler.toml"
7
+ ],
8
+ "truth": [
9
+ "component:default/edge",
10
+ "component:default/telemetry-api",
11
+ "constraint:edge/CONSTRAINTS.md"
12
+ ],
13
+ "critical": [
14
+ "constraint:edge/CONSTRAINTS.md"
15
+ ],
16
+ "verification": [
17
+ "verify:vitest run"
18
+ ]
19
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "c-02",
3
+ "repository": "fixture-c",
4
+ "title": "Follow the legacy backend notes while debugging a port mismatch",
5
+ "touch": ["legacy-docs/backend.md"],
6
+ "truth": ["component:default/telemetry-api", "doc:legacy-docs/backend.md", "uses:resource:default/telemetry-api-postgres"],
7
+ "critical": ["uses:resource:default/telemetry-api-postgres"],
8
+ "verification": ["verify:go test ./...", "verify:go vet ./..."]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "c-03",
3
+ "repository": "fixture-c",
4
+ "title": "Change the telemetry subject vocabulary shared across runtimes",
5
+ "touch": ["shared/index.js"],
6
+ "truth": ["component:default/shared", "component:default/edge", "component:default/console", "event:corp.telemetry.ingest.v1"],
7
+ "critical": ["event:corp.telemetry.ingest.v1"],
8
+ "verification": []
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "c-04",
3
+ "repository": "fixture-c",
4
+ "title": "Add an operation to the telemetry ingest contract",
5
+ "touch": ["wire/telemetry.openapi.yaml"],
6
+ "truth": ["api:default/telemetry-ingest-api", "component:default/telemetry-api", "provides:api:default/telemetry-ingest-api"],
7
+ "critical": ["provides:api:default/telemetry-ingest-api"],
8
+ "verification": ["verify:go test ./..."]
9
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "id": "koda-01",
3
+ "repository": "koda",
4
+ "title": "Modify one API consumer of the accounts M2M contract",
5
+ "touch": [
6
+ "apps/calendar/src/lib/accounts-client.ts"
7
+ ],
8
+ "truth": [
9
+ "component:default/calendar",
10
+ "consumes:api:default/accounts-m2m-api",
11
+ "consumes:api:default/accounts-oidc-api",
12
+ "uses:resource:default/calendar-postgres",
13
+ "doc:apps/calendar/README.md",
14
+ "system:default/school-calendar"
15
+ ],
16
+ "critical": [
17
+ "consumes:api:default/accounts-m2m-api",
18
+ "consumes:api:default/accounts-oidc-api",
19
+ "uses:resource:default/calendar-postgres"
20
+ ],
21
+ "verification": [
22
+ "verify:turbo run test --filter=@koda/calendar",
23
+ "verify:turbo run typecheck --filter=@koda/calendar",
24
+ "verify:pnpm --filter @koda/calendar test"
25
+ ]
26
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "id": "koda-02",
3
+ "repository": "koda",
4
+ "title": "Change a database schema in the notifications service",
5
+ "touch": [
6
+ "apps/notifications/internal/db/migrations/0007_add_channel.sql"
7
+ ],
8
+ "truth": [
9
+ "component:default/notifications",
10
+ "uses:resource:default/notifications-postgres",
11
+ "system:default/identity-access"
12
+ ],
13
+ "critical": [
14
+ "uses:resource:default/notifications-postgres"
15
+ ],
16
+ "verification": [
17
+ "verify:go test ./...",
18
+ "verify:go vet ./..."
19
+ ]
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "id": "koda-03",
3
+ "repository": "koda",
4
+ "title": "Change how a service authenticates against accounts",
5
+ "touch": [
6
+ "apps/notifications/internal/circle/circle.go"
7
+ ],
8
+ "truth": [
9
+ "component:default/notifications",
10
+ "consumes:api:default/accounts-oidc-api",
11
+ "uses:resource:default/notifications-postgres"
12
+ ],
13
+ "critical": [
14
+ "consumes:api:default/accounts-oidc-api",
15
+ "uses:resource:default/notifications-postgres"
16
+ ],
17
+ "verification": [
18
+ "verify:go test ./...",
19
+ "verify:go vet ./..."
20
+ ]
21
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "koda-04",
3
+ "repository": "koda",
4
+ "title": "Add a producer for the canonical lesson artifact event",
5
+ "touch": [
6
+ "apps/teacher/src/lib/teacher-events.ts"
7
+ ],
8
+ "truth": [
9
+ "component:default/teacher",
10
+ "event:koda.lesson.published"
11
+ ],
12
+ "critical": [
13
+ "event:koda.lesson.published"
14
+ ],
15
+ "verification": [
16
+ "verify:turbo run test --filter=@koda/teacher",
17
+ "verify:pnpm --filter @koda/teacher test"
18
+ ]
19
+ }