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
package/core/audit.mjs ADDED
@@ -0,0 +1,407 @@
1
+ // Repository audit and migration report.
2
+ //
3
+ // MIGRATE mode does not trust documentation. It inventories four sources of
4
+ // truth — the repository's manifests and code, its machine-readable contracts,
5
+ // its authored documentation, and its authoritative external systems — and then
6
+ // reports, per fact, which of these supports it. Every major fact lands in
7
+ // exactly one class:
8
+ //
9
+ // CONFIRMED two or more independent sources agree
10
+ // DERIVED deterministically derivable from the repository
11
+ // OBSERVED read from an authoritative external system
12
+ // CONFLICT sources disagree and no authority rule settles it
13
+ // UNRESOLVED the repository cannot answer, and neither can we
14
+ //
15
+ // Uncertainty is never promoted to authored truth. `agentdoc scaffold` writes
16
+ // only CONFIRMED and DERIVED facts; OBSERVED stays in an ObservationSet;
17
+ // CONFLICT and UNRESOLVED go into the reviewed-ambiguity list for a human.
18
+ import { classifyContractFile } from "./contracts.mjs";
19
+ import { ContextIndex } from "./query.mjs";
20
+ import { expandGlob, isRepoPath } from "./fsx.mjs";
21
+
22
+ const DOC_EXT = /\.(md|mdx|adoc|rst|txt)$/;
23
+
24
+ export function audit(repo, cfg, res) {
25
+ const graph = res.graph;
26
+ const idx = new ContextIndex(graph);
27
+ const classes = new Map(); // fact -> class
28
+ const note = (fact, cls, why) => {
29
+ if (!classes.has(fact)) classes.set(fact, { cls, why: [] });
30
+ const cur = classes.get(fact);
31
+ if (cls === "CONFLICT" || cls === "UNRESOLVED") cur.cls = cls;
32
+ if (why) cur.why.push(why);
33
+ };
34
+
35
+ // 1. entities: confirmed when corroborated by a real artifact on disk
36
+ for (const e of graph.entities) {
37
+ const f = e.ref;
38
+ if (e.kind === "Component") {
39
+ const unit = res.discovery.units.find((u) => u.component && u.component.ref === e.ref);
40
+ const markers = (unit && unit.markers) || [];
41
+ if (markers.length >= 1) note(f, "CONFIRMED", "descriptor + " + markers.map((m) => m.adapter).join(","));
42
+ else note(f, "UNRESOLVED", "descriptor exists but no artifact evidence was found in " + (e.derived?.sourcePath || "?"));
43
+ } else if (e.kind === "API") {
44
+ const cref = e.entity.spec.contract?.ref;
45
+ if (e.entity.spec.type === "oidc") note(f, "CONFIRMED", "oidc surface validated against provider source and discovery path");
46
+ else if (cref && repo.exists(cref)) {
47
+ const consumers = graph.relations.filter((r) => r.type === "consumesApi" && r.targetRef === f);
48
+ note(f, consumers.length ? "CONFIRMED" : "DERIVED", "canonical contract " + cref + (consumers.length ? " with " + consumers.length + " discovered consumer(s)" : " with no discovered consumer"));
49
+ } else note(f, "UNRESOLVED", "API has no resolvable canonical contract");
50
+ } else {
51
+ note(f, "DERIVED", "authored placement entity");
52
+ }
53
+ }
54
+
55
+ // 2. facts and conflicts
56
+ for (const a of graph.assertions) {
57
+ const f = a.subject + " " + a.key;
58
+ if (a.evidenceClass === "OBSERVED_RUNTIME") note(f, "OBSERVED", a.observed ? a.observed.sourceSystem + " at " + a.observed.at : "runtime");
59
+ else if (a.status === "unresolved") note(f, "UNRESOLVED", "no deterministic resolution");
60
+ else if (a.status === "candidate") note(f, "UNRESOLVED", "heuristic candidate only");
61
+ else if (a.evidenceClass === "AUTHORED") note(f, "DERIVED", "authored");
62
+ else if (a.confidence === "deterministic") note(f, "DERIVED", "deterministic extraction");
63
+ else note(f, "DERIVED", a.evidenceClass);
64
+ }
65
+ for (const c of graph.conflicts) {
66
+ const f = c.subject + " " + c.key;
67
+ // A divergence a rule settled is a known fact with a documented basis, not
68
+ // something awaiting a human. Reporting it as CONFLICT would put settled
69
+ // questions in the reviewed-ambiguity list forever.
70
+ const settled = c.status === "resolved" || c.status === "accepted";
71
+ note(f, settled ? "DERIVED" : "CONFLICT",
72
+ c.kind + ": " + (c.detail || "") + " (" + c.status +
73
+ (settled ? ", elected " + (c.election.basis || "?") + " by " + (c.election.ruleId || "?") : "") + ")");
74
+ }
75
+
76
+ // 3. relations
77
+ for (const r of graph.relations) {
78
+ if (r.type === "partOf" || r.type === "hasPart" || r.type === "providesApi" || r.type === "apiProvidedBy") continue;
79
+ const f = r.type + " " + r.sourceRef + " -> " + r.targetRef;
80
+ note(f, r.evidenceClass === "REVIEWED_OVERRIDE" ? "DERIVED" : "DERIVED", r.evidenceClass);
81
+ }
82
+
83
+ // 4. documentation inventory
84
+ // The configured documentation hierarchy is documentation too. A PRODUCT.md
85
+ // nobody routes to is an orphan, and saying so is the point.
86
+ //
87
+ // The exemption below is narrow on purpose. Only the fixed repository-wide
88
+ // roles are exempt, because only those are repository-wide *by definition*:
89
+ // they are reached from the configuration rather than from a component. Any
90
+ // other configured path could just as easily name a component-scoped file, and
91
+ // exempting those would let a real orphan be silenced by pointing at it in
92
+ // configuration. A `docs.architecture` entry that resolves inside a component
93
+ // root is therefore reported, not excused.
94
+ const HIERARCHY_FILE_ROLES = new Set(["product", "architecture", "constraints"]);
95
+ // Only file-shaped roles become entries. The `*Dir` keys are handled by the
96
+ // loop below; letting one through as a "document" makes `exists: true` for a
97
+ // directory, which is a check that can never fail.
98
+ const configured = Object.entries(cfg.docs || {})
99
+ .filter(([role]) => !role.endsWith("Dir"))
100
+ .map(([role, p]) => ({ role, path: p, configured: true }));
101
+ // Directory-shaped roles are enumerated rather than listed as a single entry:
102
+ // a directory is not a document, and reporting it as one is a dead check.
103
+ for (const [role, key] of [["adr", "adrDir"], ["runbook", "runbookDir"]]) {
104
+ const dir = (cfg.docs || {})[key];
105
+ if (!dir) continue;
106
+ if (repo.isDir(dir)) {
107
+ for (const f of repo.walk(dir)) if (DOC_EXT.test(f)) configured.push({ role, path: f, configured: false });
108
+ } else {
109
+ // A directory role that does not exist is reported through `exists:false`
110
+ // below, so it needs no separate flag here.
111
+ configured.push({ role, path: dir, configured: false });
112
+ }
113
+ }
114
+ const unitRoots = new Set(res.discovery.units.map((u) => u.root));
115
+ const insideComponent = (p) => {
116
+ for (const r of unitRoots) if (p === r || p.startsWith(r + "/")) return r;
117
+ return null;
118
+ };
119
+
120
+ // The audit reports; it does not gate. These are therefore collected as audit
121
+ // findings on the report rather than pushed into the compile-time diagnostics,
122
+ // where a `warning` would be gated by `check` and a migration could not even
123
+ // see what it is being asked to fix. Each is emitted with the same code a
124
+ // compile-time gate would use, so the vocabulary is shared.
125
+ const docFindings = [];
126
+ const docInventory = [];
127
+ for (const f of repo.walk("")) {
128
+ if (!DOC_EXT.test(f)) continue;
129
+ const owners = graph.entities.filter((e) => {
130
+ const d = e.derived || {};
131
+ return (d.docs || []).includes(f) || (d.constraints || []).includes(f) || (d.runbooks || []).includes(f);
132
+ }).map((e) => e.ref);
133
+ const role = (configured.find((h) => h.path === f) || {}).role || null;
134
+ docInventory.push({ path: f, linkedTo: owners.sort(), role });
135
+ }
136
+
137
+ // Built from the *configured* list, not from the walk, so a configured path
138
+ // that does not exist is reported. Filtering the walk made `exists` a constant
139
+ // `true`, which is the one thing a reader would want it to catch.
140
+ const hierarchyDocs = configured.map((h) => ({
141
+ role: h.role,
142
+ path: h.path,
143
+ exists: repo.exists(h.path),
144
+ // The component root this path sits inside, when it sits inside one. A
145
+ // repository-wide role that resolves inside a component is the case worth
146
+ // reporting, so this is computed for every entry rather than only for the
147
+ // ones that look exempt.
148
+ componentScoped: insideComponent(h.path),
149
+ }));
150
+
151
+ const exempt = (d) => {
152
+ if (d.linkedTo.length) return true;
153
+ if (d.role && HIERARCHY_FILE_ROLES.has(d.role) && !insideComponent(d.path)) return true;
154
+ return false;
155
+ };
156
+ const orphanDocs = docInventory.filter((d) => !exempt(d)).map((d) => d.path);
157
+ for (const d of docInventory.filter((x) => !exempt(x))) {
158
+ docFindings.push({
159
+ severity: "warning",
160
+ code: "AGENTDOC_DOC_ORPHANED",
161
+ subject: d.path,
162
+ refs: [d.path],
163
+ message:
164
+ "document is claimed by no component" +
165
+ (d.role ? " and its configured role (" + d.role + ") is not a repository-wide role" : "") +
166
+ ". Link it from a component descriptor, or remove it.",
167
+ });
168
+ }
169
+ for (const h of hierarchyDocs) {
170
+ if (!h.exists) {
171
+ docFindings.push({
172
+ severity: "warning",
173
+ code: "AGENTDOC_DOC_ORPHANED",
174
+ subject: h.path,
175
+ refs: [h.path],
176
+ message: "configured documentation path does not exist: " + h.path,
177
+ });
178
+ } else if (h.componentScoped) {
179
+ docFindings.push({
180
+ severity: "warning",
181
+ code: "AGENTDOC_DOC_ORPHANED",
182
+ subject: h.path,
183
+ refs: [h.path],
184
+ message:
185
+ "configured as repository-wide documentation (" + h.role + ") but resolves inside component " +
186
+ h.componentScoped + ". Either move it or claim it from that component's descriptor.",
187
+ });
188
+ }
189
+ }
190
+
191
+ // 5. contract inventory
192
+ const claimedContracts = new Set(
193
+ graph.entities.filter((e) => e.kind === "API" && e.entity.spec.contract?.ref).map((e) => e.entity.spec.contract.ref)
194
+ );
195
+ const contractInventory = [];
196
+ const scanRoots = new Set([
197
+ ...(cfg.discovery.contractRoots || []),
198
+ ...graph.entities.filter((e) => e.kind === "API" && e.entity.spec.contract?.ref).map((e) => e.entity.spec.contract.ref.split("/").slice(0, -1).join("/")),
199
+ ]);
200
+ for (const r of scanRoots) {
201
+ if (!r || !repo.isDir(r)) continue;
202
+ for (const f of repo.walk(r)) {
203
+ if (!classifyContractFile(f)) continue;
204
+ contractInventory.push({ path: f, claimedBy: claimedContracts.has(f) ? "yes" : "no" });
205
+ }
206
+ }
207
+ const unclaimedContracts = contractInventory.filter((c) => c.claimedBy === "no").map((c) => c.path);
208
+
209
+ // 6. events without producers
210
+ const unresolvedEvents = graph.interfaces.events.filter((e) => e.resolution === "unresolved").map((e) => e.pattern);
211
+
212
+ // 7. components without documentation or verification
213
+ const undocumented = graph.entities
214
+ .filter((e) => e.kind === "Component")
215
+ .filter((e) => !((e.derived || {}).docs || []).length)
216
+ .map((e) => e.ref);
217
+ const unverified = graph.entities
218
+ .filter((e) => e.kind === "Component")
219
+ .filter((e) => !graph.verification.some((v) => v.componentRefs.includes(e.ref)))
220
+ .map((e) => e.ref);
221
+
222
+ const ambiguousOwnership = graph.conflicts
223
+ .filter((c) => c.status === "unresolved" && c.key.startsWith("placement."))
224
+ .map((c) => c.subject);
225
+
226
+ const staleObservations = graph.observations.map((o) => ({
227
+ id: o.id, capturedAt: o.capturedAt, maxAgeDays: o.maxAgeDays, environment: o.environment,
228
+ }));
229
+
230
+ // ── 8. documentation claims that contradict what the graph owns ─────────
231
+ // Only claims about vocabulary the graph actually owns are checked. A doc
232
+ // saying "there is no edge component" is checkable and worth failing on; a
233
+ // doc saying "we use Kafka" is not, because the graph has no opinion until a
234
+ // Resource descriptor says so.
235
+ const docContradictions = [];
236
+ const knownNames = new Map();
237
+ for (const e of graph.entities) knownNames.set(normaliseName(e.name), e.ref);
238
+ const portByComponent = new Map();
239
+ for (const a of graph.assertions) {
240
+ if (a.key === "component.port" && a.status === "elected") portByComponent.set(a.subject, a.value);
241
+ }
242
+ for (const d of docInventory) {
243
+ if (!/\.(md|mdx|adoc|rst)$/.test(d.path)) continue;
244
+ const owners = d.linkedTo;
245
+ let text;
246
+ try {
247
+ text = repo.readText(d.path);
248
+ } catch {
249
+ continue;
250
+ }
251
+ for (const m of text.matchAll(/\b(?:there (?:is|are)|we have)\s+no\s+([a-z][a-z0-9-]{2,30})\b/gi)) {
252
+ const ref = knownNames.get(normaliseName(m[1]));
253
+ if (ref) {
254
+ docContradictions.push({
255
+ kind: "NEGATED_COMPONENT",
256
+ doc: d.path,
257
+ claim: m[0].trim(),
258
+ contradicts: ref,
259
+ detail: "the document denies a component the graph proves exists",
260
+ });
261
+ }
262
+ }
263
+ for (const m of text.matchAll(/\bport\s*(\d{4,5})\b/gi)) {
264
+ const claimed = Number(m[1]);
265
+ for (const owner of owners) {
266
+ const actual = portByComponent.get(owner);
267
+ if (actual !== undefined && actual !== claimed) {
268
+ docContradictions.push({
269
+ kind: "PORT_MISMATCH",
270
+ doc: d.path,
271
+ claim: m[0].trim(),
272
+ contradicts: owner,
273
+ detail: "the document states port " + claimed + "; the source binds port " + actual,
274
+ });
275
+ }
276
+ }
277
+ if (!owners.length) {
278
+ docContradictions.push({
279
+ kind: "UNLINKED_PORT_CLAIM",
280
+ doc: d.path,
281
+ claim: m[0].trim(),
282
+ contradicts: null,
283
+ detail: "the document states a port but is not linked to any component, so the claim cannot be checked",
284
+ });
285
+ }
286
+ }
287
+ }
288
+
289
+ // How many production dependencies exist that nobody classified as an
290
+ // external service. Reported as a number, not as hundreds of warnings: the
291
+ // decision of which third-party service is architectural belongs to a human.
292
+ const declared = new Set((cfg.discovery.externalDependencies || []).map((x) => x.name));
293
+ const unclassified = new Set();
294
+ for (const e of graph.entities) {
295
+ if (e.kind !== "Component") continue;
296
+ const unit = res.discovery.units.find((u) => u.component && u.component.ref === e.ref);
297
+ if (!unit) continue;
298
+ for (const d of Object.keys((unit.pkg || {}).dependencies || {})) {
299
+ const norm = String(d).replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
300
+ if (declared.has(norm)) continue;
301
+ if (graph.externalDependencies.some((x) => x.componentRef === e.ref)) continue;
302
+ unclassified.add(d);
303
+ }
304
+ }
305
+
306
+ const rows = [...classes.entries()].map(([fact, v]) => ({ fact, classification: v.cls, evidence: v.why.slice(0, 4) }));
307
+ rows.sort((a, b) => (a.fact < b.fact ? -1 : 1));
308
+ const counts = rows.reduce((acc, r) => { acc[r.classification] = (acc[r.classification] || 0) + 1; return acc; }, {});
309
+
310
+ return {
311
+ inventory: {
312
+ files: repo.walk("").length,
313
+ units: res.discovery.units.length,
314
+ eligibleUnits: res.discovery.eligible.length,
315
+ entities: graph.entities.length,
316
+ components: graph.entities.filter((e) => e.kind === "Component").length,
317
+ apis: graph.entities.filter((e) => e.kind === "API").length,
318
+ resources: graph.entities.filter((e) => e.kind === "Resource").length,
319
+ relations: graph.relations.length,
320
+ events: graph.interfaces.events.length,
321
+ contracts: contractInventory.length,
322
+ documentationFiles: docInventory.length,
323
+ observations: graph.observations.length,
324
+ verification: graph.verification.length + graph.gates.length,
325
+ },
326
+ facts: rows,
327
+ counts,
328
+ contradictions: [
329
+ ...graph.conflicts.filter((c) => c.status === "unresolved").map((c) => ({ kind: c.kind, subject: c.subject, key: c.key, status: c.status, detail: c.detail || null, election: c.election })),
330
+ ...docContradictions,
331
+ ],
332
+ settledDivergences: graph.conflicts
333
+ .filter((c) => c.status !== "unresolved")
334
+ .map((c) => ({ kind: c.kind, subject: c.subject, key: c.key, status: c.status, basis: c.election.basis, ruleId: c.election.ruleId })),
335
+ documentationContradictions: docContradictions,
336
+ reviewedAmbiguity: [
337
+ ...unresolvedEvents.map((pattern) => ({ kind: "EVENT_WITHOUT_PRODUCER", subject: pattern })),
338
+ ...ambiguousOwnership.map((ref) => ({ kind: "COMPONENT_OWNERSHIP", subject: ref })),
339
+ ...unclaimedContracts.map((p) => ({ kind: "UNCLAIMED_CONTRACT", subject: p })),
340
+ ...docContradictions.map((c) => ({ kind: c.kind, subject: c.doc + ": " + c.claim })),
341
+ ...(cfg.reviewedOverrides || []).map((o) => ({ kind: "REVIEWED_OVERRIDE", subject: o.subject + " " + o.fact })),
342
+ ],
343
+ documentation: {
344
+ findings: docFindings,
345
+ hierarchy: hierarchyDocs,
346
+ orphanDocs: orphanDocs.sort(),
347
+ undocumentedComponents: undocumented.sort(),
348
+ unverifiedComponents: unverified.sort(),
349
+ },
350
+ contracts: { inventory: contractInventory.sort((a, b) => (a.path < b.path ? -1 : 1)), unclaimed: unclaimedContracts.sort() },
351
+ observations: staleObservations,
352
+ unclassifiedDependencies: {
353
+ count: unclassified.size,
354
+ sample: [...unclassified].sort().slice(0, 40),
355
+ // A count a human must triage, stated once rather than as one warning per
356
+ // package: 600 rows of the same decision is not information. The code is
357
+ // in ACCEPTABLE_WARNING_CODES so the *triage* can be recorded with pinned
358
+ // evidence once someone has done it — without which no valid acceptance
359
+ // could ever be written, and the code would be decoration.
360
+ code: "AGENTDOC_EXTERNAL_DEPENDENCY_UNCLASSIFIED",
361
+ subject: null,
362
+ message:
363
+ unclassified.size + " third-party dependencies are not classified as external services. " +
364
+ "Add them under discovery.externalDependencies, or confirm they are not architectural. " +
365
+ "Every npm import is not an external service; saying which ones are is a human decision.",
366
+ },
367
+ unresolvedRefs: graph.conflicts.filter((c) => c.status === "unresolved").map((c) => c.subject + " " + c.key),
368
+ };
369
+ }
370
+
371
+ export function scaffoldProposal(repo, cfg, report) {
372
+ const files = [];
373
+ const docCandidates = repo.walk("").filter((f) => DOC_EXT.test(f) && f.split("/").length <= 3).sort();
374
+ files.push({
375
+ path: "agentdoc/agentdoc.config.yaml",
376
+ action: "create",
377
+ why: "the system has no configuration yet; this is the entry point",
378
+ });
379
+ files.push({ path: "agentdoc/journeys.yaml", action: "create-if-used", why: "journeys are the only cross-component narrative the graph cannot derive" });
380
+ files.push({ path: "agentdoc/domains.yaml", action: "scaffold", why: "domains group systems; propose from module names, confirm with a human" });
381
+ files.push({ path: "agentdoc/systems.yaml", action: "scaffold", why: "systems group components with a shared responsibility" });
382
+ files.push({ path: "agentdoc/apis.yaml", action: "scaffold", why: "one API descriptor per canonical contract" });
383
+ files.push({ path: "agentdoc/resources.yaml", action: "scaffold", why: "one Resource per logical database, dataset, bus or queue" });
384
+ for (const d of docCandidates.slice(0, 200)) {
385
+ files.push({ path: d, action: "review", why: "existing documentation must be linked to a component or declared an orphan" });
386
+ }
387
+ return {
388
+ files,
389
+ summary: {
390
+ toCreate: files.filter((f) => f.action === "create").length,
391
+ toScaffold: files.filter((f) => f.action === "scaffold").length,
392
+ toReview: files.filter((f) => f.action === "review").length,
393
+ // `counts` only carries classifications that are actually present, so an
394
+ // absent key means zero, not undefined. Summing the raw keys produced
395
+ // `null` on the flagship migration target — the one number in the
396
+ // proposal that says what must not be authored.
397
+ mustNotBeAuthored: (report.counts.CONFLICT || 0) + (report.counts.UNRESOLVED || 0),
398
+ },
399
+ rule: "only CONFIRMED and DERIVED facts may be written as authored descriptors; OBSERVED facts go to an ObservationSet; CONFLICT and UNRESOLVED stay in the reviewed-ambiguity list",
400
+ };
401
+ }
402
+
403
+ function normaliseName(s) {
404
+ return String(s).toLowerCase().replace(/[^a-z0-9]+/g, "").replace(/(api|service|worker|app|module|package|lib|library)$/g, "");
405
+ }
406
+
407
+ export { expandGlob, isRepoPath };
@@ -0,0 +1,39 @@
1
+ // Evidence classes.
2
+ //
3
+ // The whole point of this system is that "what the repository claims",
4
+ // "what can be derived from code", and "what was actually observed in a live
5
+ // system" are different kinds of claim with different failure modes. Collapsing
6
+ // them is how a catalog starts lying, so the distinction is explicit everywhere:
7
+ // in descriptors, in the graph, in query output, and in the migration report.
8
+ export const EVIDENCE_CLASSES = Object.freeze([
9
+ "AUTHORED",
10
+ "DERIVED",
11
+ "OBSERVED_RUNTIME",
12
+ "EXTERNAL_STANDARD",
13
+ "REVIEWED_OVERRIDE",
14
+ "UNRESOLVED",
15
+ ]);
16
+
17
+ // Confidence is a separate axis from evidence class. It records *how* the claim
18
+ // was obtained, independently of who could vouch for it.
19
+ export const CONFIDENCE = Object.freeze([
20
+ "direct", // read straight from the named authority system
21
+ "deterministic", // reproducible function of committed inputs
22
+ "declared", // asserted by a human-maintained document
23
+ "reviewed", // a reviewer signed off on an interpretation
24
+ "candidate", // heuristic; never allowed to become an elected fact
25
+ ]);
26
+
27
+ // The single most important rule in the system.
28
+ export const CANDIDATE = "candidate";
29
+
30
+ // Map provenance record class -> evidence class, used when materialising
31
+ // relations and indexes.
32
+ export const PROV_TO_EVIDENCE = Object.freeze({
33
+ declared: "AUTHORED",
34
+ reviewed: "REVIEWED_OVERRIDE",
35
+ observed: "DERIVED",
36
+ validated: "DERIVED",
37
+ runtime: "OBSERVED_RUNTIME",
38
+ standard: "EXTERNAL_STANDARD",
39
+ });