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,881 @@
1
+ // The canonical compilation pipeline. One code path for validate, compile,
2
+ // check, query, impact and audit — locally and in CI. No behaviour depends on
3
+ // how it was invoked.
4
+ import { AgentDocError, CODES, collect } from "./codes.mjs";
5
+ import { Repo, computeInputHash, sha256Hex } from "./fsx.mjs";
6
+ import { loadSchemaBundle, loadConfig, loadDescriptors, loadJourneys, loadObservations, loadReviewedOverrides, makeRefs, CONFIG_PATH } from "./descriptors.mjs";
7
+ import { discoverUnits, matchComponents } from "./discover.mjs";
8
+ import { genericFacts, checkArtifactType } from "./derive.mjs";
9
+ import { buildRelations } from "./relations.mjs";
10
+ import { buildIndexes, checkContractCoverage } from "./indexes.mjs";
11
+ import { ProvRegistry } from "./provenance.mjs";
12
+ import { ingestObservations } from "./observations.mjs";
13
+ import { FactStore } from "./facts.mjs";
14
+ import { AuthorityEngine, resolveAssertions } from "./authority/engine.mjs";
15
+ import { PROV_TO_EVIDENCE } from "./authority/classes.mjs";
16
+ import { redactCommand } from "./secrets.mjs";
17
+ import { formatErrors } from "./jsonschema.mjs";
18
+ import { validateContractFile } from "./contracts.mjs";
19
+ import { sourceFiles, importSpecifiers } from "./sourcescan.mjs";
20
+ import { applyWarningAcceptances } from "./acceptances.mjs";
21
+ import { loadAdapters } from "../adapters/index.mjs";
22
+ import {
23
+ COMPILER_NAME, COMPILER_VERSION, GRAPH_SCHEMA_VERSION,
24
+ assembleGraph, serializeGraph, finalizeSerialization, subjectResolver,
25
+ } from "./graph.mjs";
26
+
27
+ export const ADAPTER_VERSION_INPUT_PREFIX = "adapter:";
28
+
29
+ export class CompileResult {
30
+ constructor() {
31
+ this.errors = [];
32
+ this.diagnostics = [];
33
+ this.graph = null;
34
+ this.serialized = null;
35
+ this.stats = {};
36
+ this.ctx = null;
37
+ }
38
+ }
39
+
40
+ function earlyOut(res, extra = {}) {
41
+ res.stats = { ...(res.stats || {}), ...extra };
42
+ return res;
43
+ }
44
+
45
+ export function compile(root, opts = {}) {
46
+ const res = new CompileResult();
47
+ // A peek load, only to read the generated-directory list: a project that keeps
48
+ // build output in a directory this build does not know about must be able to
49
+ // say so.
50
+ let cfgGeneratedDirs = opts.generatedDirs;
51
+ try {
52
+ const peek = loadConfig(new Repo(root, { noGit: true }), loadSchemaBundle().bundle);
53
+ if (Array.isArray(peek.discovery.generatedDirs)) cfgGeneratedDirs = peek.discovery.generatedDirs;
54
+ } catch {
55
+ // No configuration, or an invalid one: the real load below reports it.
56
+ }
57
+ const repo = new Repo(root, { noGit: opts.noGit, generatedDirs: cfgGeneratedDirs });
58
+ let bundle;
59
+ let schemaInputs;
60
+ try {
61
+ ({ bundle, versionInputs: schemaInputs } = loadSchemaBundle());
62
+ } catch (e) {
63
+ if (e instanceof AgentDocError) { res.errors.push(e); return earlyOut(res, {}); }
64
+ throw e;
65
+ }
66
+ let cfg;
67
+ try {
68
+ cfg = loadConfig(repo, bundle);
69
+ } catch (e) {
70
+ // A configuration error is reported like any other, so `validate` gives a
71
+ // complete picture instead of dying on the first bad key.
72
+ if (e instanceof AgentDocError) { res.errors.push(e); return earlyOut(res, {}); }
73
+ throw e;
74
+ }
75
+ // Exposed even when the compile later fails: `scaffold` needs the
76
+ // configuration and repo handle precisely in the not-yet-compiling state —
77
+ // a coverage failure is what scaffolding exists to fix.
78
+ res.repo = repo;
79
+ res.cfg = cfg;
80
+ const refs = makeRefs(cfg.namespace);
81
+ const graphPath = cfg.output.graph;
82
+
83
+ const sources = loadDescriptors(repo, cfg, bundle, refs);
84
+ res.errors.push(...sources.errors);
85
+
86
+ const journeys = loadJourneys(repo, cfg, bundle, sources.byRef);
87
+ res.errors.push(...journeys.errors);
88
+
89
+ const observations = loadObservations(repo, cfg, bundle, sources.byRef);
90
+ res.errors.push(...observations.errors);
91
+
92
+ const overrides = loadReviewedOverrides(repo, cfg, refs, sources.byRef);
93
+ res.errors.push(...overrides.errors);
94
+
95
+ // The review record attached to every REVIEWED_OVERRIDE fact. The reviewer and
96
+ // the date are deliberately not typed in: they are the commit that changed the
97
+ // configuration carrying this record, which is stronger evidence than a string.
98
+ //
99
+ // `reviewWhen` is authorable and required, not defaulted. A fixed string here
100
+ // would be the same sentence on every override, which is a review condition
101
+ // that conditions nothing — and it would be reported as though it were real.
102
+ //
103
+ // The evidence digests are re-verified on every compile. A REVIEWED_OVERRIDE is
104
+ // the highest-authority input the model accepts, and the only thing that can
105
+ // falsify it is the evidence it cites. If that evidence is rewritten or deleted
106
+ // and the override survives, the graph ships a digest that no longer matches any
107
+ // file, and a stale human judgement is silently promoted to fact. This mirrors
108
+ // what `acceptances.mjs` already does for accepted warnings.
109
+ // The digests were verified against the pinned values in
110
+ // `loadReviewedOverrides`, so the record carried into the graph is the
111
+ // author's, already known to be current.
112
+ const reviewed = (o) => ({
113
+ reason: o.reason,
114
+ reviewWhen: o.reviewWhen,
115
+ evidence: o.evidence.map((ev) => ({ path: ev.path, sha256: ev.sha256 })),
116
+ });
117
+
118
+ res.errors.push(...checkContractCoverage({ repo, cfg, sources }));
119
+
120
+ for (const api of sources.entities.filter((e) => e.kind === "API")) {
121
+ collect(() => validateContractFile(repo, bundle, api), res.errors);
122
+ }
123
+ res.errors.push(...validateOidcProviders({ repo, sources, discovery: null }));
124
+
125
+ if (res.errors.length) {
126
+ return earlyOut(res, { entities: sources.entities.length });
127
+ }
128
+
129
+ let adapters;
130
+ try {
131
+ adapters = loadAdapters(cfg.adapters || []);
132
+ } catch (e) {
133
+ if (e instanceof AgentDocError) {
134
+ res.errors.push(e);
135
+ return earlyOut(res, { entities: sources.entities.length });
136
+ }
137
+ throw e;
138
+ }
139
+ const discovery = discoverUnits(repo, cfg, adapters);
140
+ // Eligibility is the compiler's own answer to "which directories are units" —
141
+ // exposed on the result so scaffold can write descriptors for exactly these
142
+ // roots rather than re-deriving them from filesystem markers.
143
+ res.discovery = discovery;
144
+ const match = matchComponents(discovery.eligible, sources.entities.filter((e) => e.kind === "Component"));
145
+ res.errors.push(...match.errors);
146
+
147
+ // OIDC discovery must be exposed by the provider's own source. Re-check now
148
+ // that component -> unit bindings are known.
149
+ res.errors.push(...validateOidcProviders({ repo, sources, discovery }));
150
+
151
+ if (res.errors.length) {
152
+ return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
153
+ }
154
+
155
+ const prov = new ProvRegistry(repo);
156
+ const diagnostics = [];
157
+ const facts = new FactStore();
158
+ const edges = [];
159
+ const events = new Map();
160
+ const externals = new Map();
161
+ const capabilities = new Map();
162
+ const verification = new Map();
163
+
164
+ const ctx = {
165
+ repo, cfg, refs, bundle, prov, diagnostics, facts,
166
+ sources: { ...sources, journeys: journeys.journeys }, discovery, journeys: journeys.journeys, journeysPath: journeys.path,
167
+ observations, overrides: overrides.overrides,
168
+ edges, events, externals, capabilities, verification,
169
+ errors: [],
170
+ unitByComponent: new Map(),
171
+ secondPasses: [],
172
+ };
173
+ ctx.errors = res.errors;
174
+
175
+ const addEdge = (type, sourceRef, targetRef, attributes, provRecs, evidenceClass) => {
176
+ const key = type + "|" + sourceRef + "|" + targetRef + "|" + JSON.stringify(attributes || {});
177
+ let e = edges.find((x) => x.key === key);
178
+ if (!e) {
179
+ e = { key, type, sourceRef, targetRef, attributes: attributes || {}, provs: new Set(), evidenceClass: evidenceClass || "DERIVED" };
180
+ edges.push(e);
181
+ }
182
+ for (const p of provRecs) if (p) e.provs.add(p);
183
+ if (evidenceClass === "REVIEWED_OVERRIDE") e.evidenceClass = "REVIEWED_OVERRIDE";
184
+ return e;
185
+ };
186
+ const addEvent = (pattern, role, compRef, provRec, contract) => {
187
+ if (!events.has(pattern)) events.set(pattern, { producers: new Set(), consumers: new Set(), provs: new Set(), contract: contract || null });
188
+ const ev = events.get(pattern);
189
+ if (role === "producer") ev.producers.add(compRef);
190
+ else ev.consumers.add(compRef);
191
+ if (contract) ev.contract = contract;
192
+ if (provRec) ev.provs.add(provRec);
193
+ };
194
+ const addExternal = (compRef, name, mechanism, role, provRec, evidenceClass) => {
195
+ const key = compRef + "|" + name;
196
+ if (!externals.has(key)) {
197
+ externals.set(key, { componentRef: compRef, name, mechanism, role, evidenceClass: evidenceClass || "DERIVED", provs: new Set() });
198
+ }
199
+ externals.get(key).provs.add(provRec);
200
+ };
201
+ const addCapability = (compRef, name, provRec, evidenceClass) => {
202
+ const key = compRef + "|" + name;
203
+ if (!capabilities.has(key)) {
204
+ capabilities.set(key, { componentRef: compRef, name, evidenceClass: evidenceClass || "DERIVED", provs: new Set() });
205
+ }
206
+ capabilities.get(key).provs.add(provRec);
207
+ };
208
+ const addVerification = (v) => {
209
+ if (!v.componentRefs || v.componentRefs.length === 0) return;
210
+ // A verification command is a routing index, not a credential transport.
211
+ v = { ...v, command: redactCommand(String(v.command || "").trim()).slice(0, 600) };
212
+ if (!verification.has(v.id)) verification.set(v.id, { ...v, provs: new Set(), componentRefs: new Set(v.componentRefs) });
213
+ const cur = verification.get(v.id);
214
+ for (const p of v.provRecs || []) cur.provs.add(p);
215
+ for (const r of v.componentRefs) cur.componentRefs.add(r);
216
+ };
217
+ const gates = new Map();
218
+ const addGate = (g) => {
219
+ g = { ...g, command: redactCommand(String(g.command || "").trim()).slice(0, 600) };
220
+ if (!gates.has(g.id)) gates.set(g.id, { ...g, provs: new Set() });
221
+ for (const p of g.provRecs || []) gates.get(g.id).provs.add(p);
222
+ };
223
+ ctx.addEdge = addEdge;
224
+ ctx.addEvent = addEvent;
225
+ ctx.addExternal = addExternal;
226
+ ctx.addCapability = addCapability;
227
+ ctx.addVerification = addVerification;
228
+ ctx.addGate = addGate;
229
+ ctx.gates = gates;
230
+ ctx.deferSecondPass = (fn) => ctx.secondPasses.push(fn);
231
+ ctx.unitForRoot = (root) => discovery.units.find((u) => u.root === root);
232
+ ctx.primaryEventContract = () => {
233
+ const ec = (cfg.discovery.eventContracts || [])[0];
234
+ if (!ec) return null;
235
+ return { format: ec.format === "code" ? "code" : "declared-catalog", ref: ec.path };
236
+ };
237
+ ctx.addFact = (subject, key, value, opts) => facts.add({ subject, key, value, ...opts });
238
+
239
+ // External-service classification is declared, never inferred. Compiled once
240
+ // so every adapter sees the same matchers in the same order.
241
+ const externalMatchers = ((cfg.discovery.externalDependencies || []).map((x) => {
242
+ let re;
243
+ try {
244
+ re = new RegExp(x.match);
245
+ } catch {
246
+ throw new AgentDocError(CODES.CONFIG, "external dependency pattern is not a valid regular expression: " + x.match);
247
+ }
248
+ return { re, name: x.name, mechanism: x.mechanism, role: x.role, resourceType: x.resourceType || null };
249
+ })).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
250
+ ctx.externalMatchers = () => externalMatchers;
251
+
252
+ // The declared client name that owns a Resource of the given type, when exactly
253
+ // one does.
254
+ //
255
+ // A component depending on a `redis` client can reach one Resource two ways: a
256
+ // `cache-client` route that spots the import, and a `declared-client` route
257
+ // that matches the configured external dependency. Those are the same
258
+ // dependency. When only the declared route carried `client`, the two had
259
+ // different identities and never merged, so one dependency shipped as two
260
+ // edges — the very duplication the identity rule exists to prevent.
261
+ //
262
+ // Keyed on the Resource's type rather than on a package name, because the
263
+ // mechanism adapters detect by pattern over file *text* and have no package
264
+ // name to match against. Returns undefined when the type is unclaimed or
265
+ // ambiguous, and the mechanism edge then stands alone rather than guessing.
266
+ ctx.clientKeyForResourceType = (resourceType) => {
267
+ if (!resourceType) return undefined;
268
+ const claims = externalMatchers.filter((x) => x.resourceType === resourceType);
269
+ return claims.length === 1 ? claims[0].name : undefined;
270
+ };
271
+
272
+ // ── manifest pre-pass: identify each unit's package / module namespace ────
273
+ // Done before extraction so cross-unit resolution never depends on the order
274
+ // in which units happen to be processed.
275
+ for (const unit of discovery.eligible) {
276
+ const pkgPath = unit.root + "/package.json";
277
+ if (repo.exists(pkgPath)) {
278
+ try {
279
+ const pkg = repo.readJson(pkgPath);
280
+ unit.pkgName = typeof pkg.name === "string" ? pkg.name : null;
281
+ unit.pkg = pkg;
282
+ } catch {
283
+ res.errors.push(new AgentDocError(CODES.SCHEMA, "package.json is not valid JSON: " + pkgPath, { path: pkgPath }));
284
+ }
285
+ }
286
+ const gomodPath = unit.root + "/go.mod";
287
+ if (repo.exists(gomodPath)) {
288
+ const m = /^module\s+(\S+)/m.exec(repo.readText(gomodPath));
289
+ unit.goModule = m ? m[1] : null;
290
+ }
291
+ }
292
+ const unitByPkgName = new Map();
293
+ const unitByGoModule = new Map();
294
+ for (const unit of discovery.eligible) {
295
+ if (unit.pkgName && !unitByPkgName.has(unit.pkgName)) unitByPkgName.set(unit.pkgName, unit);
296
+ if (unit.goModule && !unitByGoModule.has(unit.goModule)) unitByGoModule.set(unit.goModule, unit);
297
+ }
298
+ ctx.unitByPkgName = unitByPkgName;
299
+ ctx.unitByGoModule = unitByGoModule;
300
+
301
+ // Manifest-declared unit dependencies, computed before any adapter runs so
302
+ // that cross-unit reasoning never depends on adapter execution order.
303
+ // Production dependency names per unit, so a "depends on this service" rule
304
+ // matches a dependency name rather than a blob of source.
305
+ const productionDependencies = new Map();
306
+ const dependencyMap = new Map();
307
+ for (const unit of discovery.eligible) {
308
+ if (!unit.component) continue;
309
+ const deps = new Set();
310
+ const pkg = unit.pkg || {};
311
+ productionDependencies.set(unit.component.ref, [
312
+ ...Object.keys(pkg.dependencies || {}),
313
+ ...Object.keys(pkg.optionalDependencies || {}),
314
+ ]);
315
+ for (const d of [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})]) {
316
+ const t = unitByPkgName.get(d);
317
+ if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
318
+ }
319
+ if (unit.goModule) {
320
+ const gomod = repo.exists(unit.root + "/go.mod") ? repo.readText(unit.root + "/go.mod") : "";
321
+ const goReqs = [...gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)].map((m) => m[1]);
322
+ productionDependencies.set(unit.component.ref, [...(productionDependencies.get(unit.component.ref) || []), ...goReqs]);
323
+ for (const m of gomod.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)) {
324
+ const t = unitByGoModule.get(m[1]);
325
+ if (t && t.component && t.component.ref !== unit.component.ref) deps.add(t.component.ref);
326
+ }
327
+ }
328
+ dependencyMap.set(unit.component.ref, deps);
329
+ }
330
+ ctx.dependencyMap = dependencyMap;
331
+ ctx.productionDependencies = (ref) => productionDependencies.get(ref) || [];
332
+ ctx.transitiveDependencies = (ref) => {
333
+ const out = new Set([ref]);
334
+ const stack = [ref];
335
+ while (stack.length) {
336
+ const cur = stack.pop();
337
+ for (const d of dependencyMap.get(cur) || []) {
338
+ if (!out.has(d)) { out.add(d); stack.push(d); }
339
+ }
340
+ }
341
+ return out;
342
+ };
343
+
344
+ // ── cross-unit import index (language-neutral scan, adapter-specific use) ─
345
+ const importIndex = buildImportIndex(repo, discovery);
346
+ ctx.importIndex = importIndex;
347
+
348
+ // ── per-unit derivation ──────────────────────────────────────────────────
349
+ const derivedMap = new Map();
350
+ for (const unit of discovery.eligible) {
351
+ const comp = unit.component;
352
+ if (!comp) continue;
353
+ const compRef = comp.ref;
354
+ ctx.unitByComponent.set(compRef, unit);
355
+ const unitCtx = {
356
+ ...ctx,
357
+ compRef,
358
+ unitImports: importIndex.byUnit.get(unit) || new Map(),
359
+ descriptorPath: () => comp.file,
360
+ componentEntity: () => comp,
361
+ };
362
+ const d = genericFacts(unitCtx, unit);
363
+ for (const a of adapters) {
364
+ if (a.extract) {
365
+ try {
366
+ const extra = a.extract(unitCtx, unit) || {};
367
+ mergeDerived(d, extra);
368
+ } catch (e) {
369
+ if (e instanceof AgentDocError) res.errors.push(e);
370
+ else throw e;
371
+ }
372
+ }
373
+ }
374
+ d.runtimes = [...(d.runtimes || new Set())].sort();
375
+ d.languages = [...(d.languages || new Set())].sort();
376
+ derivedMap.set(compRef, d);
377
+ diagnostics.push(...checkArtifactType(comp, unit));
378
+ // Authored-field provenance for every entity, not just components.
379
+ prov.add("declared", comp.file, "descriptor-reader", [
380
+ "ent:" + compRef, "entf:" + compRef + ":/apiVersion", "entf:" + compRef + ":/kind",
381
+ "entf:" + compRef + ":/metadata", "entf:" + compRef + ":/metadata/name",
382
+ "entf:" + compRef + ":/metadata/description", "entf:" + compRef + ":/spec",
383
+ ]);
384
+ }
385
+ for (const e of sources.entities) {
386
+ if (e.kind === "Component") continue;
387
+ const subjects = [
388
+ "ent:" + e.ref, "entf:" + e.ref + ":/apiVersion", "entf:" + e.ref + ":/kind",
389
+ "entf:" + e.ref + ":/metadata", "entf:" + e.ref + ":/metadata/name",
390
+ "entf:" + e.ref + ":/metadata/description",
391
+ ];
392
+ if (e.doc.spec !== undefined) subjects.push("entf:" + e.ref + ":/spec");
393
+ prov.add("declared", e.file, "descriptor-reader", subjects);
394
+ }
395
+
396
+ // ── contract adapters run once, after every unit is known ───────────────
397
+ const contractOut = [];
398
+ ctx.addContract = (rec) => {
399
+ if (!rec || !rec.apiRef || !rec.ref) return;
400
+ // A contract entry is a pointer, but the pointer itself is a claim about the
401
+ // repository and is corroborated by the file it names.
402
+ contractOut.push({ ...rec, provs: new Set([prov.add("observed", rec.ref, "contract-extractor", ["ctr:" + rec.apiRef])]) });
403
+ };
404
+ for (const a of adapters) {
405
+ if (!a.contracts) continue;
406
+ try {
407
+ a.contracts(ctx);
408
+ } catch (e) {
409
+ if (e instanceof AgentDocError) res.errors.push(e);
410
+ else throw e;
411
+ }
412
+ }
413
+ for (const e of ctx.errors) if (!res.errors.includes(e)) res.errors.push(new AgentDocError(e.code, e.message, { path: e.path }));
414
+
415
+ // ── second pass: cross-unit resolutions that need the full picture ──────
416
+ const byPlatformName = new Map();
417
+ for (const u of discovery.eligible) if (u.platformName) byPlatformName.set(u.platformName, u);
418
+ for (const fn of ctx.secondPasses) {
419
+ try {
420
+ fn({ byPlatformName });
421
+ } catch (e) {
422
+ if (e instanceof AgentDocError) res.errors.push(e);
423
+ else throw e;
424
+ }
425
+ }
426
+
427
+ // ── authored placement facts ─────────────────────────────────────────────
428
+ for (const e of sources.entities) {
429
+ const spec = e.doc.spec || {};
430
+ const mk = () => prov.add("declared", e.file, "placement-extractor", ["ass:pending"]);
431
+ if ((e.kind === "Component" || e.kind === "System") && spec.domain) {
432
+ facts.add({ subject: e.ref, key: "placement.domain", value: spec.domain, evidenceClass: "AUTHORED", confidence: "declared", provRecs: [mk()] });
433
+ }
434
+ if ((e.kind === "Component" || e.kind === "Resource") && spec.system) {
435
+ facts.add({ subject: e.ref, key: "placement.system", value: spec.system, evidenceClass: "AUTHORED", confidence: "declared", provRecs: [mk()] });
436
+ }
437
+ }
438
+
439
+ // ── reviewed overrides → relations and facts ─────────────────────────────
440
+ const overrideEdges = [];
441
+ for (const o of overrides.overrides) {
442
+ // Which pending subject the evidence must resolve to depends on whether
443
+ // this override produces a relation or a fact.
444
+ const producesRelation = o.fact === "runtime-call" || o.fact === "api-consumer" || o.fact === "external-dependency-role";
445
+ const subject = producesRelation ? "rel:pending" : "ass:pending";
446
+ const provs = [
447
+ ...o.evidence.map((ev) => prov.add("declared", ev.path, "override-extractor", [subject])),
448
+ prov.add("reviewed", CONFIG_PATH, "override-extractor", [subject]),
449
+ ];
450
+ if (o.fact === "runtime-call") {
451
+ const attrs = { transport: o.transport, contractRef: o.contractRef };
452
+ if (o.deliverySemantics) attrs.deliverySemantics = o.deliverySemantics;
453
+ overrideEdges.push({ type: "runtimeCalls", sourceRef: o.subject, targetRef: o.target, attributes: attrs, provs });
454
+ facts.add({
455
+ subject: o.subject, key: "runtime-call." + String(o.target).split("/")[1], value: attrs,
456
+ evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs,
457
+ semantics: o.reason,
458
+ review: reviewed(o),
459
+ });
460
+ } else if (o.fact === "api-consumer") {
461
+ overrideEdges.push({ type: "consumesApi", sourceRef: o.subject, targetRef: o.target, attributes: { via: "reviewed-override" }, provs });
462
+ facts.add({
463
+ subject: o.subject, key: "api-consumer." + String(o.target).split("/")[1], value: String(o.target),
464
+ evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs, semantics: o.reason,
465
+ review: reviewed(o),
466
+ });
467
+ } else if (o.fact === "external-dependency-role") {
468
+ const rec = externals.get(o.subject + "|" + o.target);
469
+ if (!rec) {
470
+ res.errors.push(new AgentDocError(
471
+ CODES.RELATION_KIND,
472
+ "external-dependency-role override targets no observed external dependency: " + o.subject + " -> " + o.target,
473
+ { path: CONFIG_PATH, ref: o.subject }
474
+ ));
475
+ } else {
476
+ rec.role = o.role;
477
+ rec.evidenceClass = "REVIEWED_OVERRIDE";
478
+ for (const p of provs) rec.provs.add(p);
479
+ facts.add({
480
+ subject: o.subject, key: "external-role." + o.target, value: o.role,
481
+ evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs, semantics: o.reason,
482
+ review: reviewed(o),
483
+ });
484
+ }
485
+ } else if (o.fact === "binding") {
486
+ facts.add({
487
+ subject: o.subject, key: "binding.kind", value: o.kind,
488
+ evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs,
489
+ semantics: o.reason,
490
+ review: reviewed(o),
491
+ });
492
+ } else if (o.fact === "capability") {
493
+ addCapability(o.subject, o.kind, provs[0], "REVIEWED_OVERRIDE");
494
+ facts.add({
495
+ subject: o.subject, key: "capability." + o.kind, value: true,
496
+ evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs, semantics: o.reason,
497
+ review: reviewed(o),
498
+ });
499
+ } else if (o.fact === "event-producer" || o.fact === "event-consumer") {
500
+ addEvent(o.eventPattern, o.fact === "event-producer" ? "producer" : "consumer", o.subject, provs[0], ctx.primaryEventContract());
501
+ facts.add({
502
+ subject: o.subject, key: "event-role." + o.eventPattern, value: o.fact === "event-producer" ? "producer" : "consumer",
503
+ evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs, semantics: o.reason,
504
+ review: reviewed(o),
505
+ });
506
+ } else if (o.fact === "component-placement") {
507
+ const key = o.system ? "placement.system" : "placement.domain";
508
+ facts.add({
509
+ subject: o.subject, key, value: o.system || o.domain,
510
+ evidenceClass: "REVIEWED_OVERRIDE", confidence: "reviewed", provRecs: provs,
511
+ semantics: o.reason,
512
+ review: reviewed(o),
513
+ });
514
+ }
515
+ }
516
+ ctx.overrideEdges = overrideEdges;
517
+
518
+ // ── observations → OBSERVED_RUNTIME facts ────────────────────────────────
519
+ const observationOut = ingestObservations(ctx);
520
+
521
+ // ── authority resolution ─────────────────────────────────────────────────
522
+ const authority = new AuthorityEngine(cfg.authority.rules);
523
+
524
+ // Supersession is applied before election: an observation that a later set
525
+ // explicitly replaces does not compete for the election, though it stays in
526
+ // the graph as history.
527
+ // Keyed on capturedAt+environment: batch captures across environments can
528
+ // share a timestamp, and a timestamp-only key would mark the wrong set's
529
+ // facts superseded.
530
+ const supersededAt = new Set();
531
+ for (const o of observationOut) {
532
+ if (o.supersedes) {
533
+ const prior = observationOut.find((x) => x.id === o.supersedes);
534
+ if (prior) supersededAt.add(prior.capturedAt + "\u0000" + prior.environment);
535
+ }
536
+ }
537
+ for (const f of facts.facts) {
538
+ if (f.evidenceClass !== "OBSERVED_RUNTIME") continue;
539
+ const fo = f.observed;
540
+ if (!fo || !supersededAt.has(fo.at + "\u0000" + fo.environment)) continue;
541
+ f.status = "superseded";
542
+ // Supersession is a claim a human made, so it is a reviewable claim. Silence
543
+ // would let any observation quietly void any other: the code is in
544
+ // ACCEPTABLE_WARNING_CODES precisely so the disposition can be recorded
545
+ // against pinned evidence rather than left implicit.
546
+ diagnostics.push({
547
+ severity: "warning",
548
+ code: "AGENTDOC_OBSERVATION_SUPERSEDED",
549
+ subject: f.subject,
550
+ refs: [f.subject],
551
+ message:
552
+ "runtime observation captured " + (f.observed && f.observed.at) + " was superseded by a later capture " +
553
+ "and no longer competes for the election. It remains in the graph as history. " +
554
+ "Confirm the superseding capture really replaces it.",
555
+ });
556
+ }
557
+
558
+ const { conflicts: rawConflicts } = resolveAssertions(facts.facts, authority);
559
+ pushConflictDiagnostics(rawConflicts, diagnostics, facts);
560
+
561
+ // ── relations ────────────────────────────────────────────────────────────
562
+ const rel = buildRelations(ctx);
563
+ res.errors.push(...rel.errors);
564
+
565
+ // A monorepo task graph and a package script often cover the same tier. Two
566
+ // runnable commands for one check is duplication an agent has to read past, so
567
+ // the preference is explicit and the loser is dropped with its provenance.
568
+ const pref = cfg.discovery.verificationPreference || "task-graph";
569
+ if (pref !== "both") {
570
+ const taskGraphTiers = new Set();
571
+ for (const id of verification.keys()) {
572
+ if (!id.startsWith("tg:")) continue;
573
+ const v = verification.get(id);
574
+ for (const r of v.componentRefs) taskGraphTiers.add(r + "|" + v.tier);
575
+ }
576
+ for (const [id, v] of [...verification.entries()]) {
577
+ if (id.startsWith("tg:") || id.startsWith("ci:")) continue;
578
+ const shadowed = [...v.componentRefs].some((r) => taskGraphTiers.has(r + "|" + v.tier));
579
+ if (!shadowed) continue;
580
+ verification.delete(id);
581
+ // The dropped entry's evidence must stop claiming to prove it, or the
582
+ // provenance record would point at a verification entry that no longer
583
+ // exists.
584
+ for (const p of v.provs) p.subjects.delete("ver:" + id);
585
+ }
586
+ }
587
+
588
+ const indexes = buildIndexes(ctx);
589
+ indexes.gates = [...gates.values()]
590
+ .sort((a, b) => (a.id < b.id ? -1 : 1))
591
+ .map((g) => ({ id: g.id, tier: g.tier, command: g.command, configPaths: [...(g.configPaths || [])].sort(), provs: g.provs }));
592
+ indexes.contracts = contractOut.slice().sort((a, b) => (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0));
593
+
594
+ res.errors.push(...applyWarningAcceptances(repo, cfg.warningAcceptances || [], diagnostics));
595
+
596
+ // Bind pending provenance subjects now that indices exist.
597
+ rel.relations.forEach((r) => {
598
+ const key = r.type + "|" + r.sourceRef + "|" + r.targetRef + "|" + JSON.stringify(r.attributes || {});
599
+ for (const p of r.provs) {
600
+ p.subjects.delete("rel:pending");
601
+ p.subjects.add("rel:" + key);
602
+ }
603
+ });
604
+ for (const f of facts.facts) {
605
+ for (const p of f.provRecs) {
606
+ p.subjects.delete("ass:pending");
607
+ p.subjects.add("ass:" + f.id);
608
+ }
609
+ }
610
+ // Safety net: evidence whose candidate edge or fact was dropped (an
611
+ // unresolved override, a rejected candidate) legitimately has no subject.
612
+ // Records that end up proving nothing are removed rather than left dangling,
613
+ // and the count is reported so a regression here is visible.
614
+ let droppedProvenance = 0;
615
+ for (let i = prov.records.length - 1; i >= 0; i--) {
616
+ const rec = prov.records[i];
617
+ rec.subjects.delete("rel:pending");
618
+ rec.subjects.delete("ass:pending");
619
+ if (rec.subjects.size === 0) {
620
+ prov.records.splice(i, 1);
621
+ droppedProvenance++;
622
+ }
623
+ }
624
+
625
+ // error-severity diagnostics block publication
626
+ for (const dg of diagnostics) {
627
+ if (dg.severity === "error") res.errors.push(new AgentDocError(dg.code, dg.message, { path: dg.paths?.[0], ref: dg.refs?.[0] }));
628
+ }
629
+ if (res.errors.length) {
630
+ return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
631
+ }
632
+
633
+ diagnostics.sort((a, b) => {
634
+ const ka = a.severity + "|" + a.code + "|" + (a.paths?.[0] || "") + "|" + a.message;
635
+ const kb = b.severity + "|" + b.code + "|" + (b.paths?.[0] || "") + "|" + b.message;
636
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
637
+ });
638
+
639
+ // Nested provenance (bindings/health) needs the finalized ids.
640
+ const skeleton = assembleGraph(sources, rel, indexes, derivedMap, facts, rawConflicts, observationOut, { idOf: new Map(), list: [] }, diagnostics);
641
+ let provFinal;
642
+ try {
643
+ provFinal = prov.finalize(subjectResolver(skeleton));
644
+ } catch (e) {
645
+ res.errors.push(e instanceof AgentDocError ? e : new AgentDocError(CODES.PROVENANCE, e.message));
646
+ return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
647
+ }
648
+ for (const d of derivedMap.values()) {
649
+ for (const list of [d.bindings || [], d.healthContracts || []]) {
650
+ for (const b of list) {
651
+ b.provenanceIds = [...(b._provs || [])].map((p) => provFinal.idOf.get(p)).filter(Boolean).sort();
652
+ delete b._provs;
653
+ }
654
+ }
655
+ }
656
+
657
+ const graph = assembleGraph(sources, rel, indexes, derivedMap, facts, rawConflicts, observationOut, provFinal, diagnostics);
658
+ graph.compiler = { name: COMPILER_NAME, version: COMPILER_VERSION, adapters: adapterStamp(adapters) };
659
+
660
+ const commit = repo.headCommit();
661
+ const inputHash = computeInputHash(repo, [
662
+ COMPILER_NAME + "@" + COMPILER_VERSION,
663
+ GRAPH_SCHEMA_VERSION,
664
+ "agentdoc.dev/v1",
665
+ "authority-rules@" + cfg.authority.rules.length,
666
+ ...schemaInputs,
667
+ ...adapters.map((a) => ADAPTER_VERSION_INPUT_PREFIX + a.name + "@" + a.version),
668
+ ]);
669
+ const dirtySet = new Set(repo.dirtyPaths().filter((p) => p !== graphPath));
670
+ const walkedDirs = [...repo.walks.keys()];
671
+ const dirty = [...dirtySet].some(
672
+ (p) =>
673
+ repo.readPaths.has(p) ||
674
+ repo.probes.has(p) ||
675
+ repo.listings.has(p) ||
676
+ walkedDirs.some((d) => (d === "." ? true : p === d || p.startsWith(d + "/")))
677
+ );
678
+ graph.source = { commit, dirty, inputHash };
679
+
680
+ const gv = bundle.validator("agentdoc.dev/schema/graph.schema.json");
681
+ const gerrs = gv.validate(graph);
682
+ if (gerrs) {
683
+ res.errors.push(new AgentDocError(
684
+ CODES.GRAPH_SCHEMA,
685
+ "compiled graph violates the graph schema — compiler defect: " + formatErrors(gerrs)
686
+ ));
687
+ return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
688
+ }
689
+
690
+ let serialized;
691
+ try {
692
+ serialized = finalizeSerialization(serializeGraph(graph), graph, {
693
+ // Only timestamps the compiler read out of a committed ObservationSet may
694
+ // appear. This is what keeps compilation a pure function of the checkout.
695
+ allowedTimestamps: observationOut.map((o) => o.capturedAt),
696
+ });
697
+ } catch (e) {
698
+ res.errors.push(e instanceof AgentDocError ? e : new AgentDocError(CODES.NONDETERMINISTIC, e.message));
699
+ return earlyOut(res, { entities: sources.entities.length, units: discovery.eligible.length });
700
+ }
701
+
702
+ res.graph = graph;
703
+ res.serialized = serialized;
704
+ res.diagnostics = diagnostics;
705
+ res.graphPath = graphPath;
706
+ res.sources = sources;
707
+ res.indexes = indexes;
708
+ res.facts = facts;
709
+ res.conflicts = graph.conflicts;
710
+ res.stats = {
711
+ entities: graph.entities.length,
712
+ components: graph.entities.filter((e) => e.kind === "Component").length,
713
+ relations: graph.relations.length,
714
+ events: graph.interfaces.events.length,
715
+ externals: graph.externalDependencies.length,
716
+ capabilities: graph.capabilities.length,
717
+ verification: graph.verification.length,
718
+ journeys: graph.journeys.length,
719
+ assertions: graph.assertions.length,
720
+ conflicts: graph.conflicts.length,
721
+ unresolvedConflicts: graph.conflicts.filter((c) => c.status === "unresolved").length,
722
+ observations: graph.observations.length,
723
+ provenance: graph.provenance.length,
724
+ units: discovery.eligible.length,
725
+ acceptedWarnings: diagnostics.filter((d) => d.severity === "warning" && d.acceptance).length,
726
+ unacceptedWarnings: diagnostics.filter((d) => d.severity === "warning" && !d.acceptance).length,
727
+ droppedProvenance,
728
+ inputHash,
729
+ commit,
730
+ dirty,
731
+ };
732
+ return res;
733
+ }
734
+
735
+ // A contradiction that no authority rule can settle is an error: the system
736
+ // refuses to route an agent onto a subject whose facts disagree. A divergence
737
+ // that a rule or a reviewed override *does* settle is a warning, acceptable
738
+ // with review, so the divergence stays visible in the graph either way.
739
+ // The review record attached to every REVIEWED_OVERRIDE fact. The reviewer and
740
+ // the date are deliberately not typed in: they are the commit that changed the
741
+ // configuration carrying this record, which is stronger evidence than a string.
742
+ // degradeOnStale is deliberately NOT applied here: a rule's answer to "what
743
+ // does a stale election mean" depends on the wall clock, and a compile must be
744
+ // a pure function of the checkout. Degradation is evaluated at gate time, in
745
+ // the same place observation freshness itself is enforced (see the CLI's
746
+ // freshness gate). Serializing a day-count or a degraded status into the graph
747
+ // would make identical inputs compile to different bytes on different days.
748
+ function pushConflictDiagnostics(rawConflicts, diagnostics, facts) {
749
+ const byId = new Map(facts.facts.map((f) => [f.id, f]));
750
+ for (const c of rawConflicts) {
751
+ const values = c.assertionIds
752
+ .map((id) => byId.get(id))
753
+ .filter(Boolean)
754
+ .map((f) => f.evidenceClass + "=" + JSON.stringify(f.value))
755
+ .sort();
756
+ const detail = c.subject + " " + c.key + ": " + values.join(" vs ");
757
+ if (c.kind === "ambiguity" && c.status === "unresolved") {
758
+ diagnostics.push({
759
+ severity: "warning",
760
+ code: CODES.UNRESOLVED_FACT,
761
+ subject: c.subject,
762
+ refs: [c.subject],
763
+ message:
764
+ "unresolved fact — " + detail + ". " + (c.election.rationale || "") +
765
+ ". Author the owning entity, add a reviewed override, or accept with review.",
766
+ });
767
+ continue;
768
+ }
769
+ if (c.status === "unresolved") {
770
+ diagnostics.push({
771
+ severity: "error",
772
+ code: CODES.CONFLICT_UNRESOLVED,
773
+ subject: c.subject,
774
+ refs: [c.subject],
775
+ message:
776
+ "unresolved " + c.kind + " — " + detail + ". " +
777
+ (c.election.rationale || "no authority rule governs this fact") +
778
+ ". Add an authority rule and, if deterministic discovery cannot settle it, a reviewed override.",
779
+ });
780
+ } else {
781
+ diagnostics.push({
782
+ severity: "warning",
783
+ code: "AGENTDOC_CONFLICT_REVIEWED",
784
+ subject: c.subject,
785
+ refs: [c.subject],
786
+ message:
787
+ c.kind + " — " + detail + ". " +
788
+ (c.election.elected
789
+ ? "elected " + c.election.elected.evidenceClass + " via " + c.election.basis + " (rule " + c.election.ruleId + ")"
790
+ : "not elected") +
791
+ ". The contradicted value remains in the graph.",
792
+ });
793
+ }
794
+ }
795
+ }
796
+
797
+ // A Go import is either host.tld/owner/repo/path or host.tld/owner/repo. The
798
+ // second form is what a Go *workspace* member uses, and it has only two dots —
799
+ // a pattern that requires three silently loses every workspace-only dependency.
800
+ const GO_IMPORT_RE = /["`]([a-z0-9]+\.[a-z0-9.-]+\/[a-z0-9./_-]+|[a-z0-9]+\.[a-z0-9]+\.[a-z0-9./-]+)["`]/g;
801
+
802
+ // Language-neutral scan of every unit's production sources, indexing the
803
+ // external specifiers each file mentions. Adapters decide which of those
804
+ // specifiers are sibling units and what kind of relation that implies.
805
+ function buildImportIndex(repo, discovery) {
806
+ const byUnit = new Map();
807
+ const goPathsByUnit = new Map();
808
+ for (const unit of discovery.eligible) {
809
+ const map = new Map();
810
+ const goPaths = new Set();
811
+ for (const f of sourceFiles(repo, unit.root, { test: false })) {
812
+ const specifiers = importSpecifiers(repo.readText(f));
813
+ for (const s of specifiers) {
814
+ if (!map.has(s)) map.set(s, new Set());
815
+ map.get(s).add(f);
816
+ }
817
+ if (unit.goModule) {
818
+ const text = repo.readText(f);
819
+ // Every module-shaped import is recorded, not only those under the
820
+ // unit's own module: a Go workspace member can be depended on purely by
821
+ // importing it, with no require line at all.
822
+ for (const m of text.matchAll(GO_IMPORT_RE) ) goPaths.add(m[1]);
823
+ }
824
+ }
825
+ byUnit.set(unit, map);
826
+ goPathsByUnit.set(unit, goPaths);
827
+ }
828
+ return { byUnit, goPathsByUnit };
829
+ }
830
+
831
+ function mergeDerived(target, extra) {
832
+ for (const [k, v] of Object.entries(extra || {})) {
833
+ if (v instanceof Set) {
834
+ target[k] = target[k] instanceof Set ? new Set([...target[k], ...v]) : new Set(v);
835
+ } else if (Array.isArray(v)) {
836
+ target[k] = (target[k] || []).concat(v);
837
+ } else if (v && typeof v === "object") {
838
+ target[k] = { ...(target[k] || {}), ...v };
839
+ } else if (v !== undefined && v !== null) {
840
+ target[k] = v;
841
+ }
842
+ }
843
+ }
844
+
845
+ function adapterStamp(adapters) {
846
+ return adapters
847
+ .map((a) => ({ name: a.name, version: a.version }))
848
+ .sort((a, b) => (a.name < b.name ? -1 : 1));
849
+ }
850
+
851
+ function validateOidcProviders({ repo, sources, discovery }) {
852
+ const errors = [];
853
+ for (const api of sources.entities.filter((e) => e.kind === "API" && e.doc.spec.type === "oidc")) {
854
+ const dp = api.doc.spec.contract.discoveryPath;
855
+ const provider = sources.byRef.get(api.doc.spec.provider);
856
+ const unit = discovery && discovery.eligible.find((x) => x.component === provider);
857
+ if (!provider) continue;
858
+ const roots = unit ? [unit.root] : [dirOf(provider.file)];
859
+ let found = false;
860
+ for (const root of roots) {
861
+ for (const f of repo.walk(root)) {
862
+ if (!/\.(ts|tsx|js|mjs|jsx|py|go|rb|java|kt|rs)$/.test(f)) continue;
863
+ if (/(^|\/)(test|tests|__tests__)\//.test(f) || /\.(test|spec)\./.test(f)) continue;
864
+ if (repo.readText(f).includes(dp)) { found = true; break; }
865
+ }
866
+ if (found) break;
867
+ }
868
+ if (!found) {
869
+ errors.push(new AgentDocError(
870
+ CODES.OIDC_DISCOVERY,
871
+ "provider " + api.doc.spec.provider + " does not expose discovery path '" + dp + "' anywhere in its source",
872
+ { path: api.file, ref: api.ref }
873
+ ));
874
+ }
875
+ }
876
+ return errors;
877
+ }
878
+
879
+ function dirOf(p) { return p.split("/").slice(0, -1).join("/") || "."; }
880
+
881
+ export { redactCommand, PROV_TO_EVIDENCE, sha256Hex };