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,86 @@
1
+ // Generic adapter: language- and toolchain-agnostic facts only.
2
+ //
3
+ // Nothing here knows a language, a package manager, a provider or a folder
4
+ // name. It contributes the two things any repository can be said to have:
5
+ // conventional documentation (handled in core/derive.mjs) and health contract
6
+ // route literals found in source.
7
+ import { Adapter } from "./registry.mjs";
8
+ import { resolveClientResources } from "./resource-client.mjs";
9
+ import { sourceFiles } from "../core/sourcescan.mjs";
10
+
11
+ // Conservative: a health route is an absolute string literal that looks like a
12
+ // liveness or readiness endpoint. Anything more specific belongs to a
13
+ // framework adapter.
14
+ const HEALTH_LITERAL = /["'`](\/(?:healthz|health|livez|readyz|ready|ping|status))["'`]/;
15
+
16
+ export class GenericAdapter extends Adapter {
17
+ static adapterName = "generic";
18
+ constructor() {
19
+ super({ name: "generic", version: "1.0.0", kind: "source" });
20
+ }
21
+
22
+ extract(ctx, unit) {
23
+ const { repo, prov, compRef } = ctx;
24
+ const derived = { healthContracts: [] };
25
+ const seen = new Set();
26
+ const ports = new Set();
27
+ const portFiles = new Map();
28
+ const allSrc = sourceFiles(repo, unit.root, { test: false });
29
+ for (const f of allSrc) {
30
+ const text = repo.readText(f);
31
+ const pm = /ListenAndServe\(\s*":(\d{2,5})"|\bPORT\s*=\s*"?(\d{2,5})"?|\bport:\s*(\d{2,5})\b/.exec(text);
32
+ if (pm) {
33
+ const p = pm[1] || pm[2] || pm[3];
34
+ ports.add(p);
35
+ if (!portFiles.has(p)) portFiles.set(p, f);
36
+ }
37
+ const m = HEALTH_LITERAL.exec(text);
38
+ if (!m) continue;
39
+ const route = m[1];
40
+ if (seen.has(route)) continue;
41
+ seen.add(route);
42
+ const pr = prov.add("observed", f, "health-extractor", ["der:" + compRef + ":/healthContracts"]);
43
+ derived.healthContracts.push({ path: f, route, semantics: "liveness", provenanceIds: null, _provs: new Set([pr]) });
44
+ }
45
+ if (derived.healthContracts.length > 1) derived.healthContracts.sort((a, b) => (a.route < b.route ? -1 : 1));
46
+ // A port is only a fact when the source states exactly one. Two literals
47
+ // mean the component binds more than one, and a single number would be a
48
+ // confident half-truth.
49
+ // Declared external services front Resources; resolve those bindings.
50
+ // Provenance must name a file that exists. The dependency evidence is the
51
+ // manifest when there is one, and otherwise the unit's descriptor.
52
+ const manifest = ["package.json", "go.mod", "Cargo.toml", "pyproject.toml", "pom.xml"]
53
+ .map((n) => unit.root + "/" + n)
54
+ .find((p) => repo.exists(p)) || ctx.componentEntity().file;
55
+ const clientProv = prov.add("observed", manifest, "client-resolver", ["rel:pending", "der:" + compRef + ":/bindings"]);
56
+ resolveClientResources(ctx, unit, compRef, clientProv);
57
+
58
+ // Capability detectors are declared, not hard-coded: a manifest cannot tell
59
+ // you a component can use a browser API, but a source literal can.
60
+ for (const cap of (ctx.cfg.discovery.capabilities || [])) {
61
+ let re;
62
+ try {
63
+ re = new RegExp(cap.match);
64
+ } catch {
65
+ continue;
66
+ }
67
+ for (const f of allSrc) {
68
+ if (!re.test(repo.readText(f))) continue;
69
+ const cpr = prov.add("observed", f, "capability-extractor", ["cap:" + compRef + "|" + cap.name]);
70
+ ctx.addCapability(compRef, cap.name, cpr, "DERIVED");
71
+ break;
72
+ }
73
+ }
74
+
75
+ if (ports.size === 1) {
76
+ const p = [...ports][0];
77
+ const pr = prov.add("observed", portFiles.get(p) || ctx.componentEntity().file, "port-extractor", ["der:" + compRef + ":/port"]);
78
+ derived.port = Number(p);
79
+ ctx.addFact(compRef, "component.port", Number(p), {
80
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
81
+ semantics: "the single TCP port the source binds a listener to",
82
+ });
83
+ }
84
+ return derived;
85
+ }
86
+ }
@@ -0,0 +1,106 @@
1
+ // GitHub Actions adapter.
2
+ //
3
+ // CI is a verification authority: a workflow that names a component's paths is
4
+ // the check that must be run for that component. Recording the mapping means
5
+ // an agent changing a component is told which gate covers it, instead of
6
+ // guessing or running everything.
7
+ import { Adapter } from "./registry.mjs";
8
+ import { parseYamlDocuments } from "../core/yaml.mjs";
9
+
10
+ export class GitHubActionsAdapter extends Adapter {
11
+ static adapterName = "github-actions";
12
+ constructor() {
13
+ super({ name: "github-actions", version: "1.0.0", kind: "ci" });
14
+ }
15
+ detect(ctx, root) {
16
+ const p = root + "/.github/workflows";
17
+ if (!ctx.repo.isDir(p)) return null;
18
+ return { path: p, shape: "importable" };
19
+ }
20
+ contracts(ctx) {
21
+ const dir = ".github/workflows";
22
+ if (!ctx.repo.isDir(dir)) return [];
23
+ const out = [];
24
+ const files = ctx.repo.walk(dir).filter((f) => /\.ya?ml$/.test(f)).sort();
25
+ const units = ctx.discovery.eligible.filter((u) => u.component);
26
+ for (const f of files) {
27
+ let text = "";
28
+ try {
29
+ text = ctx.repo.readText(f);
30
+ } catch {
31
+ continue;
32
+ }
33
+ let doc = null;
34
+ try {
35
+ doc = parseYamlDocuments(text, f)[0]?.doc;
36
+ } catch {
37
+ doc = null;
38
+ }
39
+ const steps = [];
40
+ if (doc && doc.jobs) {
41
+ for (const [jobName, job] of Object.entries(doc.jobs)) {
42
+ for (const step of (job && job.steps) || []) {
43
+ const run = [step.run, step.uses].filter(Boolean).join(" ");
44
+ if (!run) continue;
45
+ steps.push({ job: jobName, run: String(run) });
46
+ }
47
+ }
48
+ }
49
+ const covered = new Set();
50
+ for (const u of units) {
51
+ if (text.includes(u.root + "/") || (u.pkgName && text.includes(u.pkgName))) covered.add(u.component.ref);
52
+ }
53
+ const pr = ctx.prov.add("observed", f, "ci-extractor", []);
54
+ const subjects = [];
55
+ steps.forEach((s, i) => {
56
+ if (!/[a-z0-9]/i.test(s.run)) return;
57
+ const command = s.run.trim().slice(0, 600);
58
+ // A step that installs dependencies or checks out code is not a check.
59
+ // Recording it as one would bury the commands that actually gate.
60
+ const tier = ciTier(command);
61
+ if (!tier) return;
62
+ const id = "ci:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
63
+ if (covered.size) {
64
+ ctx.addVerification({
65
+ id,
66
+ componentRefs: [...covered].sort(),
67
+ tier,
68
+ command,
69
+ configPaths: [f],
70
+ provRecs: [pr],
71
+ });
72
+ subjects.push("ver:" + id);
73
+ } else {
74
+ // A gate that names no component is repository-wide: it applies to
75
+ // everything, so it is recorded once as a gate rather than
76
+ // duplicated onto every component.
77
+ // The graph id must not itself start with the provenance prefix, or
78
+ // the subject key becomes ambiguous. "repo-gate:" is unambiguous.
79
+ const gid = "repo-gate:" + f.replace(/[^A-Za-z0-9]+/g, "-") + ":" + s.job + ":" + i;
80
+ ctx.addGate({ id: gid, tier: ciTier(command), command, configPaths: [f], provRecs: [pr] });
81
+ subjects.push("gate:" + gid);
82
+ }
83
+ });
84
+ for (const s of subjects) pr.subjects.add(s);
85
+ void steps;
86
+ void covered;
87
+ }
88
+ return out;
89
+ }
90
+ }
91
+
92
+ // Returns null for a step that is not a check. Install, checkout, cache and
93
+ // upload steps gate nothing and would only add noise to a routing surface.
94
+ const NOT_A_CHECK = /^\s*(?:-\s*)?(?:uses:\s*actions\/(?:checkout|cache|upload|download|setup-|artifact)|run:\s*(?:\w*\s*)?(?:pnpm|npm|yarn|bun)\s+(?:i|install|ci)\b)/i;
95
+
96
+ function ciTier(run) {
97
+ if (NOT_A_CHECK.test(run.trim())) return null;
98
+ if (/^uses:/.test(run.trim()) && !/check|lint|test|build|audit|scan/i.test(run)) return null;
99
+ if (/e2e|playwright|cypress/.test(run)) return "e2e";
100
+ if (/contract|openapi|api:check|drift|catalog/.test(run)) return "contract";
101
+ if (/integration/.test(run)) return "integration";
102
+ if (/lint|typecheck|type-check|golangci|vet/.test(run)) return "static";
103
+ if (/\btest\b|pytest|vitest|jest|go test/.test(run)) return "unit";
104
+ if (/\bbuild\b|tsc|compile/.test(run)) return "build";
105
+ return null;
106
+ }
@@ -0,0 +1,91 @@
1
+ // Go adapter: go.mod / go.work as unit markers, module graph as build
2
+ // relations, and the go toolchain's own verification commands.
3
+ import { Adapter } from "./registry.mjs";
4
+
5
+ export class GoAdapter extends Adapter {
6
+ static adapterName = "go";
7
+ constructor() {
8
+ super({ name: "go", version: "1.0.0", kind: "unit-marker" });
9
+ }
10
+ roots(ctx) {
11
+ const out = [];
12
+ const ws = "go.work";
13
+ if (!ctx.repo.exists(ws)) return out;
14
+ const text = ctx.repo.readText(ws);
15
+ const block = /use\s*\(([^)]*)\)/s.exec(text);
16
+ const candidates = block
17
+ ? block[1].split("\n").map((l) => l.replace(/\/\/.*$/, "").trim()).filter(Boolean)
18
+ : [...text.matchAll(/^\s*use\s+(\S+)/gm)].map((m) => m[1]);
19
+ for (const raw of candidates) {
20
+ const d = raw.replace(/^\.\//, "").replace(/\/$/, "");
21
+ if (!d || d.startsWith("//") || d.includes("..")) continue;
22
+ if (!ctx.repo.isDir(d)) continue;
23
+ if (!ctx.repo.exists(d + "/go.mod")) continue;
24
+ out.push({ root: d, deployable: ctx.repo.isDir(d + "/cmd"), importable: !ctx.repo.isDir(d + "/cmd") });
25
+ }
26
+ return out;
27
+ }
28
+ detect(ctx, root) {
29
+ const p = root + "/go.mod";
30
+ if (!ctx.repo.exists(p)) return null;
31
+ return { path: p, shape: ctx.repo.isDir(root + "/cmd") ? "deployable" : "importable" };
32
+ }
33
+ extract(ctx, unit) {
34
+ const { repo, prov, compRef } = ctx;
35
+ const p = unit.root + "/go.mod";
36
+ if (!repo.exists(p)) return {};
37
+ const derived = { languages: new Set(["go"]), runtimes: new Set(["go"]) };
38
+ const text = repo.readText(p);
39
+ const pr = prov.add("observed", p, "gomod-extractor", ["der:" + compRef + ":/runtimes", "der:" + compRef + ":/languages"]);
40
+
41
+ for (const m of text.matchAll(/^\s*(?:require\s+)?([a-z0-9.]+\.[a-z0-9./-]+)\s+v[0-9]/gm)) {
42
+ const mod = m[1];
43
+ const target = ctx.unitByGoModule.get(mod);
44
+ if (target && target.component && target.component.ref !== compRef) {
45
+ ctx.addEdge("buildDependsOn", compRef, target.component.ref, { via: "go-module" }, [pr], "DERIVED");
46
+ } else {
47
+ for (const x of ctx.externalMatchers()) {
48
+ if (!x.re.test(mod)) continue;
49
+ ctx.addExternal(compRef, x.name, x.mechanism, x.role, pr, "DERIVED");
50
+ }
51
+ }
52
+ }
53
+ // Workspace members depended on by import alone. A Go workspace member can
54
+ // have no require line at all, so the import graph is the only evidence.
55
+ for (const importPath of ctx.importIndex.goPathsByUnit.get(unit) || []) {
56
+ for (const [mod, target] of ctx.unitByGoModule) {
57
+ if (target === unit || !target.component) continue;
58
+ if (importPath !== mod && importPath.startsWith(mod + "/")) {
59
+ ctx.addEdge("buildDependsOn", compRef, target.component.ref, { via: "go-import" }, [pr], "DERIVED");
60
+ }
61
+ }
62
+ }
63
+
64
+ const hasTests = repo.walk(unit.root).some((f) => /_test\.go$/.test(f));
65
+ ctx.addVerification({
66
+ id: compRef + ":go-test",
67
+ componentRefs: [compRef],
68
+ tier: "unit",
69
+ command: "go test ./...",
70
+ configPaths: [p],
71
+ provRecs: [pr],
72
+ });
73
+ ctx.addVerification({
74
+ id: compRef + ":go-vet",
75
+ componentRefs: [compRef],
76
+ tier: "static",
77
+ command: "go vet ./...",
78
+ configPaths: [p],
79
+ provRecs: [pr],
80
+ });
81
+ if (hasTests) derived.hasTests = true;
82
+ derived.goModule = unit.goModule;
83
+ return derived;
84
+ }
85
+ }
86
+
87
+ export function goExternalName(mod) {
88
+ const parts = mod.split("/");
89
+ const tail = parts[parts.length - 1].replace(/\.v\d+$/, "");
90
+ return (tail || parts[0]).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "go-module";
91
+ }
@@ -0,0 +1,28 @@
1
+ // GraphQL adapter: SDL as the canonical contract plus deterministic consumer
2
+ // resolution.
3
+ import { Adapter } from "./registry.mjs";
4
+ import { resolveContractConsumers, isBuildConfig } from "./contract-consumers.mjs";
5
+
6
+ export class GraphQLAdapter extends Adapter {
7
+ static adapterName = "graphql";
8
+ constructor() {
9
+ super({ name: "graphql", version: "1.0.0", kind: "contract" });
10
+ }
11
+ contracts(ctx) {
12
+ const out = [];
13
+ for (const api of ctx.sources.entities.filter((e) => e.kind === "API" && ["graphql", "graphql-sdl"].includes(e.doc.spec.type))) {
14
+ const ref = api.doc.spec.contract.ref;
15
+ const text = ctx.repo.readText(ref);
16
+ const types = [...text.matchAll(/^\s*(?:type|interface|input|enum|scalar|union)\s+([A-Za-z_][A-Za-z0-9_]*)/gm)].map((m) => m[1]);
17
+ const queries = [...text.matchAll(/^\s*(?:extend\s+)?type\s+Query\s*\{([\s\S]*?)\n\}/gm)]
18
+ .flatMap((m) => [...m[1].matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[(:]/gm)].map((x) => x[1]));
19
+ ctx.addContract({ apiRef: api.ref, ref, types: [...new Set(types)].sort(), queries: [...new Set(queries)].sort() });
20
+ const consumers = resolveContractConsumers(ctx, ref, api.doc.spec.provider);
21
+ for (const [consumerRef, files] of consumers) {
22
+ const provs = files.map((f) => ctx.prov.add(isBuildConfig(f) ? "validated" : "observed", f, "api-contract-extractor", ["rel:pending"]));
23
+ ctx.addEdge("consumesApi", consumerRef, api.ref, { via: files.some(isBuildConfig) ? "codegen" : "source-reference" }, provs, "DERIVED");
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ }
@@ -0,0 +1,55 @@
1
+ // Adapter registry. Adapters are looked up by name and instantiated once.
2
+ // An unknown adapter name is a configuration error, never a silent no-op: a
3
+ // project that thinks it has protobuf discovery must not lose it quietly.
4
+ import { AgentDocError, CODES } from "../core/codes.mjs";
5
+ import { GenericAdapter } from "./generic.mjs";
6
+ import { NodeAdapter } from "./node.mjs";
7
+ import { PnpmAdapter } from "./pnpm.mjs";
8
+ import { TurborepoAdapter } from "./turborepo.mjs";
9
+ import { TypeScriptAdapter } from "./typescript.mjs";
10
+ import { GoAdapter } from "./go.mjs";
11
+ import { OpenApiAdapter } from "./openapi.mjs";
12
+ import { ProtobufAdapter } from "./protobuf.mjs";
13
+ import { GraphQLAdapter } from "./graphql.mjs";
14
+ import { OidcAdapter } from "./oidc.mjs";
15
+ import { GitHubActionsAdapter } from "./github-actions.mjs";
16
+ import { DockerfileAdapter } from "./dockerfile.mjs";
17
+ import { WranglerAdapter } from "./wrangler.mjs";
18
+ import { DatabaseAdapter } from "./database.mjs";
19
+ import { EventsAdapter } from "./events.mjs";
20
+ import { DeployConfigAdapter } from "./deploy-config.mjs";
21
+ import { ObjectStoreAdapter } from "./object-store.mjs";
22
+ import { CacheAdapter } from "./cache.mjs";
23
+
24
+ const REGISTRY = new Map();
25
+ for (const C of [
26
+ GenericAdapter, NodeAdapter, PnpmAdapter, TurborepoAdapter, TypeScriptAdapter,
27
+ GoAdapter, OpenApiAdapter, ProtobufAdapter, GraphQLAdapter, OidcAdapter,
28
+ GitHubActionsAdapter, DockerfileAdapter, WranglerAdapter, DatabaseAdapter,
29
+ EventsAdapter, DeployConfigAdapter, ObjectStoreAdapter, CacheAdapter,
30
+ ]) {
31
+ REGISTRY.set(C.prototype.constructor.adapterName, new C());
32
+ }
33
+
34
+ export function adapterNames() {
35
+ return [...REGISTRY.keys()].sort();
36
+ }
37
+
38
+ export function loadAdapters(names) {
39
+ // The generic adapter is always on: documentation conventions and health
40
+ // contract detection are language-agnostic and cost nothing.
41
+ const out = [REGISTRY.get("generic")];
42
+ for (const n of names || []) {
43
+ const a = REGISTRY.get(n);
44
+ if (!a) {
45
+ throw new AgentDocError(
46
+ CODES.CONFIG,
47
+ "unknown adapter '" + n + "'; available adapters: " + adapterNames().join(", ")
48
+ );
49
+ }
50
+ if (out.includes(a)) continue;
51
+ out.push(a);
52
+ }
53
+ // Deterministic order, independent of how the config listed them.
54
+ return out.slice().sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
55
+ }
@@ -0,0 +1,118 @@
1
+ // Node adapter: package.json as the unit marker and manifest authority.
2
+ import { Adapter } from "./registry.mjs";
3
+
4
+ // A script name maps to a verification tier. Deliberately conservative and
5
+ // project-neutral: a script that does not look like a check is not a check.
6
+ const TIERS = [
7
+ [/^(test|tests)$|^test:unit$|^test:unit:/, "unit"],
8
+ [/^test:integration$|^test:integration:/, "integration"],
9
+ [/^test:contract$|^test:contract:/, "contract"],
10
+ [/^test:e2e$|^test:e2e:/, "e2e"],
11
+ [/^test:browser$/, "unit"],
12
+ [/^(lint|typecheck|check-types|type-check)$/, "static"],
13
+ [/^(build|compile)$/, "build"],
14
+ ];
15
+ const IGNORED_SCRIPT = /^(dev|start|serve|watch|preview|clean|postinstall|preinstall|prepare|format|format:fix)$/;
16
+
17
+ export class NodeAdapter extends Adapter {
18
+ static adapterName = "node";
19
+ constructor() {
20
+ super({ name: "node", version: "1.0.0", kind: "unit-marker" });
21
+ }
22
+ detect(ctx, root) {
23
+ const p = root + "/package.json";
24
+ if (!ctx.repo.exists(p)) return null;
25
+ let pkg;
26
+ try {
27
+ pkg = ctx.repo.readJson(p);
28
+ } catch {
29
+ return null; // malformed manifest is not authority
30
+ }
31
+ const importable = Boolean(pkg.exports || pkg.main || pkg.bin || pkg.types);
32
+ return { path: p, shape: importable ? "both" : "deployable" };
33
+ }
34
+ extract(ctx, unit) {
35
+ const { repo, prov, compRef } = ctx;
36
+ const root = unit.root;
37
+ const manifestPath = root + "/package.json";
38
+ if (!repo.exists(manifestPath)) return {};
39
+ const derived = { languages: new Set(["javascript"]), runtimes: new Set(["node"]) };
40
+ const pkg = unit.pkg || repo.readJson(manifestPath);
41
+
42
+ const pr = prov.add("observed", manifestPath, "manifest-extractor", [
43
+ "der:" + compRef + ":/runtimes", "der:" + compRef + ":/languages", "der:" + compRef + ":/artifact",
44
+ ]);
45
+ const prodDeps = new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.optionalDependencies || {})]);
46
+ const devDeps = new Set(Object.keys(pkg.devDependencies || {}));
47
+
48
+ // Only declared external services are classified. A package in a manifest
49
+ // is a build fact, not an architectural dependency, and recording six
50
+ // hundred of them would make the index useless.
51
+ for (const d of [...prodDeps].sort()) {
52
+ for (const x of ctx.externalMatchers()) {
53
+ if (!x.re.test(d)) continue;
54
+ ctx.addExternal(compRef, x.name, x.mechanism, x.role, pr, "DERIVED");
55
+ }
56
+ }
57
+
58
+ // Workspace resolution: a dependency naming another discovered unit is a
59
+ // build relation, not an external dependency.
60
+ for (const d of [...prodDeps, ...devDeps].sort()) {
61
+ const target = ctx.unitByPkgName.get(d);
62
+ if (!target || !target.component) continue;
63
+ const isProd = prodDeps.has(d);
64
+ ctx.addEdge(isProd ? "buildDependsOn" : "testDependsOn", compRef, target.component.ref,
65
+ { via: "npm-manifest", package: d }, [pr], "DERIVED");
66
+ }
67
+
68
+ // Source imports are stronger evidence than the manifest, and catch
69
+ // subpath imports the manifest cannot express.
70
+ const sibling = new Map();
71
+ for (const spec of ctx.unitImports.keys()) {
72
+ const parts = spec.split("/");
73
+ const base = spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
74
+ const target = ctx.unitByPkgName.get(base);
75
+ if (!target || !target.component || target.component.ref === compRef) continue;
76
+ if (prodDeps.has(base)) continue;
77
+ if (!sibling.has(target)) sibling.set(target, new Set());
78
+ for (const f of ctx.unitImports.get(spec)) sibling.get(target).add(f);
79
+ }
80
+ for (const [target, files] of [...sibling.entries()].sort((a, b) => (a[0].root < b[0].root ? -1 : 1))) {
81
+ const provs = [...files].sort().map((f) => prov.add("observed", f, "import-extractor", ["rel:pending"]));
82
+ // The import names the same package the manifest route names, so it
83
+ // carries the same instance key. Without it, a dependency found by both
84
+ // routes shipped as two edges — the manifest one saying `package`, the
85
+ // import one not — which measures adapter overlap rather than architecture.
86
+ const pkgName = target.pkgName;
87
+ const attrs = pkgName ? { via: "source-import", package: pkgName } : { via: "source-import" };
88
+ ctx.addEdge(devDeps.has(pkgName) ? "testDependsOn" : "buildDependsOn",
89
+ compRef, target.component.ref, attrs, provs, "DERIVED");
90
+ }
91
+
92
+ for (const [name, cmd] of Object.entries(pkg.scripts || {}).sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
93
+ if (IGNORED_SCRIPT.test(name)) continue;
94
+ const tier = TIERS.find(([re]) => re.test(name));
95
+ if (!tier) continue;
96
+ const vpr = prov.add("observed", manifestPath, "verification-extractor", ["ver:" + compRef + ":" + name]);
97
+ ctx.addVerification({
98
+ id: compRef + ":" + name,
99
+ componentRefs: [compRef],
100
+ tier: tier[1],
101
+ command: String(cmd).trim(),
102
+ configPaths: [manifestPath],
103
+ provRecs: [vpr],
104
+ });
105
+ }
106
+ return derived;
107
+ }
108
+ }
109
+
110
+ // Normalise a package name into a graph-safe external dependency name.
111
+ export function externalName(d) {
112
+ const n = d
113
+ .replace(/^@[^/]+\//, "")
114
+ .replace(/[^a-zA-Z0-9]+/g, "-")
115
+ .replace(/^-+|-+$/g, "")
116
+ .toLowerCase();
117
+ return /^[a-z0-9]/.test(n) ? n.slice(0, 60) : "pkg-" + n.replace(/[^a-z0-9]/g, "").slice(0, 50);
118
+ }
@@ -0,0 +1,64 @@
1
+ // Object-store adapter: bucket bindings and literal bucket names.
2
+ //
3
+ // A bucket literal in a component's source is deterministic evidence that the
4
+ // component touches that store. It is only turned into a resource relation when
5
+ // exactly one authored logical-dataset Resource matches — an ambiguous literal
6
+ // is left unresolved rather than guessed at.
7
+ import { Adapter } from "./registry.mjs";
8
+
9
+ const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts|go|py|rb|java|kt|rs)$/;
10
+ const TEST_PATH = /(^|\/)(tests?|__tests__|spec)\//;
11
+ const TEST_FILE = /\.(test|spec)\./;
12
+ const BUCKET_LITERAL = /["'`]([a-z][a-z0-9-]{4,60})["'`]/g;
13
+ const STORE_HINT = /(@aws-sdk\/client-s3|minio|google\.cloud\.storage|\bS3\b|bucket|Bucket|BlobService|createBucket)/i;
14
+
15
+ export class ObjectStoreAdapter extends Adapter {
16
+ static adapterName = "object-store";
17
+ constructor() {
18
+ super({ name: "object-store", version: "1.0.0", kind: "source" });
19
+ }
20
+ extract(ctx, unit) {
21
+ const { repo, prov, compRef } = ctx;
22
+ const datasets = ctx.sources.entities.filter((e) => e.kind === "Resource" && e.doc.spec.type === "logical-dataset");
23
+ if (!datasets.length) return {};
24
+ const usesStore = ctx.unitImports && [...ctx.unitImports.keys()].some((k) => /s3|minio|storage|blob/i.test(k));
25
+ const bindings = [];
26
+ const claimed = new Set();
27
+ for (const f of repo.walk(unit.root)) {
28
+ if (!SRC_EXT.test(f)) continue;
29
+ if (TEST_PATH.test(f) || TEST_FILE.test(f)) continue;
30
+ const text = repo.readText(f);
31
+ if (!STORE_HINT.test(text)) continue;
32
+ for (const m of text.matchAll(BUCKET_LITERAL)) {
33
+ const lit = m[1];
34
+ const matches = datasets.filter((d) => lit.includes(token(d.name)) || d.name.includes(token(lit)));
35
+ if (matches.length !== 1) {
36
+ if (matches.length > 1) {
37
+ ctx.diagnostics.push({
38
+ severity: "warning",
39
+ code: "AGENTDOC_BINDING_AMBIGUOUS",
40
+ subject: compRef,
41
+ refs: [compRef],
42
+ message: "bucket literal '" + lit + "' in " + f + " matches " + matches.length + " authored logical-dataset Resources; left unresolved",
43
+ paths: [f],
44
+ });
45
+ }
46
+ continue;
47
+ }
48
+ if (claimed.has(matches[0].ref)) continue;
49
+ claimed.add(matches[0].ref);
50
+ const pr = prov.add("observed", f, "binding-extractor", ["der:" + compRef + ":/bindings", "rel:pending"]);
51
+ bindings.push({ kind: "object-store", logicalResourceRef: matches[0].ref, locator: { bucket: lit }, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) });
52
+ const client = ctx.clientKeyForResourceType("logical-dataset");
53
+ const attrs = client ? { via: "bucket-literal", client } : { via: "bucket-literal" };
54
+ ctx.addEdge("usesResource", compRef, matches[0].ref, attrs, [pr], "DERIVED");
55
+ }
56
+ }
57
+ if (!usesStore && !bindings.length) return {};
58
+ return bindings.length ? { bindings } : { objectStoreClientOnly: true };
59
+ }
60
+ }
61
+
62
+ function token(name) {
63
+ return name.split("-")[0];
64
+ }
@@ -0,0 +1,56 @@
1
+ // OIDC adapter.
2
+ //
3
+ // The canonical authority for an OIDC surface is the published standard plus
4
+ // the provider's discovery document, both of which live outside the repository
5
+ // (EXTERNAL_STANDARD). What the repository can establish is narrower and is
6
+ // recorded as such: which units implement a client against the declared
7
+ // discovery path, and the fact that the provider exposes that path.
8
+ //
9
+ // A discovery document fetched from a live deployment is an OBSERVED_RUNTIME
10
+ // fact and belongs in an ObservationSet, not here.
11
+ import { Adapter } from "./registry.mjs";
12
+ import { sourceFiles } from "../core/sourcescan.mjs";
13
+
14
+ // A component is an OIDC client when it both names the discovery surface and
15
+ // shows the shape of a token request. One of the two alone is not enough: a
16
+ // comment mentioning /oidc/token, or a bare import of an OAuth helper in a
17
+ // component that never calls the identity provider, would both produce a false
18
+ // edge, and a false edge is worse than a missing one.
19
+ const DISCOVERY_HINT = /(\.well-known\/openid-configuration|jwks_uri|token_endpoint|\/oidc\/token|oauth2?\/token)/i;
20
+ // Both naming conventions: the wire names (snake_case, as in JSON) and the
21
+ // field names a typed client uses (camelCase, as in Go and TypeScript).
22
+ const TOKEN_REQUEST_HINT = /(grant_?type|client_?id|client_?secret|jwks|authoriz|id_?token|access_?token|userinfo|refresh_?token)/i;
23
+
24
+ export class OidcAdapter extends Adapter {
25
+ static adapterName = "oidc";
26
+ constructor() {
27
+ super({ name: "oidc", version: "1.0.0", kind: "contract" });
28
+ }
29
+ contracts(ctx) {
30
+ const out = [];
31
+ for (const api of ctx.sources.entities.filter((e) => e.kind === "API" && e.doc.spec.type === "oidc")) {
32
+ const discoveryPath = api.doc.spec.contract.discoveryPath;
33
+ const providerFile = api.file;
34
+ const providerRef = api.doc.spec.provider;
35
+ const hosts = new Map();
36
+ for (const unit of ctx.discovery.eligible) {
37
+ if (!unit.component) continue;
38
+ for (const f of sourceFiles(ctx.repo, unit.root, { test: false })) {
39
+ const text = ctx.repo.readText(f);
40
+ const namesDiscovery = text.includes(discoveryPath) || DISCOVERY_HINT.test(text);
41
+ if (namesDiscovery && TOKEN_REQUEST_HINT.test(text)) {
42
+ hosts.set(unit.component.ref, f);
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ hosts.delete(providerRef);
48
+ for (const [ref, file] of [...hosts.entries()].sort()) {
49
+ const pr = ctx.prov.add("validated", file, "oidc-extractor", ["rel:pending"]);
50
+ ctx.addEdge("consumesApi", ref, api.ref, { via: "oidc-client" }, [pr], "DERIVED");
51
+ }
52
+ ctx.addContract({ apiRef: api.ref, ref: providerFile, standard: api.doc.spec.contract.standard, discoveryPath, consumers: [...hosts.keys()].sort() });
53
+ }
54
+ return out;
55
+ }
56
+ }
@@ -0,0 +1,50 @@
1
+ // OpenAPI adapter.
2
+ //
3
+ // The catalog points at the canonical contract; it never duplicates it. What
4
+ // the adapter adds is (a) the operation inventory an agent needs to find a
5
+ // handler without reading the whole spec, and (b) deterministic consumer
6
+ // resolution from the contract reference.
7
+ import { Adapter } from "./registry.mjs";
8
+ import { resolveContractConsumers, isBuildConfig } from "./contract-consumers.mjs";
9
+ import { parseYamlDocuments } from "../core/yaml.mjs";
10
+ import { parse as parseToml } from "../core/toml.mjs";
11
+
12
+ const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
13
+
14
+ export class OpenApiAdapter extends Adapter {
15
+ static adapterName = "openapi";
16
+ constructor() {
17
+ super({ name: "openapi", version: "1.0.0", kind: "contract" });
18
+ }
19
+ contracts(ctx) {
20
+ const out = [];
21
+ for (const api of ctx.sources.entities.filter((e) => e.kind === "API" && e.doc.spec.type === "openapi")) {
22
+ const ref = api.doc.spec.contract.ref;
23
+ let doc = null;
24
+ try {
25
+ doc = parseYamlDocuments(ctx.repo.readText(ref), ref)[0]?.doc || JSON.parse(ctx.repo.readText(ref, { track: false }));
26
+ } catch {
27
+ continue;
28
+ }
29
+ const operations = [];
30
+ for (const [route, item] of Object.entries((doc && doc.paths) || {})) {
31
+ for (const m of METHODS) {
32
+ if (item && typeof item === "object" && item[m]) {
33
+ operations.push({ method: m.toUpperCase(), route, operationId: item[m].operationId || null });
34
+ }
35
+ }
36
+ }
37
+ operations.sort((a, b) => (a.route + a.method < b.route + b.method ? -1 : 1));
38
+ ctx.addContract({ apiRef: api.ref, ref, operations, title: doc?.info?.title || null });
39
+
40
+ const consumers = resolveContractConsumers(ctx, ref, api.doc.spec.provider);
41
+ for (const [ref2, files] of consumers) {
42
+ const provs = files.map((f) => ctx.prov.add(isBuildConfig(f) ? "validated" : "observed", f, "api-contract-extractor", ["rel:pending"]));
43
+ ctx.addEdge("consumesApi", ref2, api.ref, { via: files.some(isBuildConfig) ? "codegen" : "source-reference" }, provs, "DERIVED");
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ }
49
+
50
+ export { parseToml };