agent-doc-system 0.0.0-stage → 1.1.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 (124) hide show
  1. package/INSTALL_FOR_AGENTS.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +98 -2
  4. package/adapters/ansible.mjs +306 -0
  5. package/adapters/cache.mjs +27 -0
  6. package/adapters/contract-consumers.mjs +56 -0
  7. package/adapters/database.mjs +116 -0
  8. package/adapters/deploy-config.mjs +34 -0
  9. package/adapters/dockerfile.mjs +26 -0
  10. package/adapters/events.mjs +145 -0
  11. package/adapters/generic.mjs +86 -0
  12. package/adapters/github-actions.mjs +106 -0
  13. package/adapters/go.mjs +91 -0
  14. package/adapters/graphql.mjs +28 -0
  15. package/adapters/index.mjs +56 -0
  16. package/adapters/node.mjs +118 -0
  17. package/adapters/object-store.mjs +64 -0
  18. package/adapters/oidc.mjs +56 -0
  19. package/adapters/openapi.mjs +50 -0
  20. package/adapters/pnpm.mjs +42 -0
  21. package/adapters/protobuf.mjs +35 -0
  22. package/adapters/registry.mjs +27 -0
  23. package/adapters/resource-client.mjs +61 -0
  24. package/adapters/turborepo.mjs +135 -0
  25. package/adapters/typescript.mjs +85 -0
  26. package/adapters/wrangler.mjs +206 -0
  27. package/bin/agentdoc.mjs +494 -0
  28. package/bin/commands.mjs +169 -0
  29. package/bin/usage.txt +28 -0
  30. package/core/acceptances.mjs +69 -0
  31. package/core/audit.mjs +456 -0
  32. package/core/authority/classes.mjs +39 -0
  33. package/core/authority/engine.mjs +266 -0
  34. package/core/authority/facts.mjs +41 -0
  35. package/core/codes.mjs +126 -0
  36. package/core/compile.mjs +1018 -0
  37. package/core/contracts.mjs +121 -0
  38. package/core/derive.mjs +102 -0
  39. package/core/descriptors.mjs +489 -0
  40. package/core/determinism.mjs +95 -0
  41. package/core/discover.mjs +147 -0
  42. package/core/facts.mjs +53 -0
  43. package/core/fsx.mjs +348 -0
  44. package/core/graph.mjs +260 -0
  45. package/core/impact.mjs +175 -0
  46. package/core/indexes.mjs +165 -0
  47. package/core/jsonschema.mjs +298 -0
  48. package/core/normative.mjs +171 -0
  49. package/core/observations.mjs +129 -0
  50. package/core/provenance.mjs +83 -0
  51. package/core/query.mjs +384 -0
  52. package/core/relations.mjs +286 -0
  53. package/core/scaffold.mjs +322 -0
  54. package/core/secrets.mjs +92 -0
  55. package/core/sourcescan.mjs +88 -0
  56. package/core/toml.mjs +163 -0
  57. package/core/yaml.mjs +477 -0
  58. package/docs/ADAPTERS.md +207 -0
  59. package/docs/AUTHORITY.md +269 -0
  60. package/docs/CI.md +138 -0
  61. package/docs/COMPARISON-KODA.md +164 -0
  62. package/docs/EVALS.md +166 -0
  63. package/docs/MIGRATION.md +261 -0
  64. package/docs/PROVENANCE.md +133 -0
  65. package/docs/QUERY.md +180 -0
  66. package/docs/SPEC.md +339 -0
  67. package/docs/VERIFICATION.md +28 -0
  68. package/docs/assets/readme/agentdoc-hero.webp +0 -0
  69. package/docs/assets/readme/agentdoc-in-action.webp +0 -0
  70. package/docs/assets/readme/how-agentdoc-works.webp +0 -0
  71. package/docs/assets/readme/why-agentdoc.webp +0 -0
  72. package/docs/ci/generic-ci.sh +28 -0
  73. package/docs/ci/github-actions.yml +35 -0
  74. package/evals/harness.mjs +426 -0
  75. package/evals/scenarios/fixture-a-01-change-api.json +9 -0
  76. package/evals/scenarios/fixture-a-02-change-db.json +9 -0
  77. package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
  78. package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
  79. package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
  80. package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
  81. package/evals/scenarios/fixture-b-04-journey.json +9 -0
  82. package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
  83. package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
  84. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
  85. package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
  86. package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
  87. package/evals/scenarios/koda-02-change-db-schema.json +20 -0
  88. package/evals/scenarios/koda-03-change-auth.json +21 -0
  89. package/evals/scenarios/koda-04-add-event-producer.json +19 -0
  90. package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
  91. package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
  92. package/evals/scenarios/koda-07-change-shared-package.json +16 -0
  93. package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
  94. package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
  95. package/evals/thresholds.json +11 -0
  96. package/package.json +46 -4
  97. package/schemas/api.schema.json +84 -0
  98. package/schemas/common.schema.json +147 -0
  99. package/schemas/component.schema.json +44 -0
  100. package/schemas/config.schema.json +801 -0
  101. package/schemas/domain.schema.json +14 -0
  102. package/schemas/graph.schema.json +1084 -0
  103. package/schemas/journeys.schema.json +47 -0
  104. package/schemas/observation.schema.json +143 -0
  105. package/schemas/resource.schema.json +28 -0
  106. package/schemas/system.schema.json +26 -0
  107. package/skills/agent-doc-system/SKILL.md +117 -0
  108. package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
  109. package/skills/agent-doc-system/references/cli.md +97 -0
  110. package/skills/agent-doc-system/references/create-migrate.md +113 -0
  111. package/skills/agent-doc-system/references/maintenance.md +52 -0
  112. package/templates/ADR.md +26 -0
  113. package/templates/ARCHITECTURE.md +19 -0
  114. package/templates/CONSTRAINTS.md +17 -0
  115. package/templates/PRODUCT.md +17 -0
  116. package/templates/api.yaml +10 -0
  117. package/templates/ci.yml +31 -0
  118. package/templates/component.yaml +14 -0
  119. package/templates/domain.yaml +6 -0
  120. package/templates/journey.yaml +7 -0
  121. package/templates/observation.yaml +19 -0
  122. package/templates/resource.yaml +8 -0
  123. package/templates/system.yaml +10 -0
  124. package/templates/warning-acceptance.yaml +14 -0
@@ -0,0 +1,85 @@
1
+ // TypeScript adapter.
2
+ //
3
+ // Owns tsconfig.json as an importability marker, project references as a build
4
+ // relation source, and the framework route conventions that are genuinely
5
+ // TypeScript-ecosystem facts rather than project conventions.
6
+ import path from "node:path";
7
+ import { Adapter } from "./registry.mjs";
8
+ import { parseYamlDocuments } from "../core/yaml.mjs";
9
+
10
+ export class TypeScriptAdapter extends Adapter {
11
+ static adapterName = "typescript";
12
+ constructor() {
13
+ super({ name: "typescript", version: "1.0.0", kind: "unit-marker" });
14
+ }
15
+ detect(ctx, root) {
16
+ const p = root + "/tsconfig.json";
17
+ if (!ctx.repo.exists(p)) return null;
18
+ return { path: p, shape: "importable" };
19
+ }
20
+ extract(ctx, unit) {
21
+ const { repo, prov, compRef } = ctx;
22
+ const p = unit.root + "/tsconfig.json";
23
+ if (!repo.exists(p)) return {};
24
+ const derived = { languages: new Set(["typescript"]), runtimes: new Set(["node"]) };
25
+ let ts;
26
+ try {
27
+ ts = parseTsconfig(repo, p);
28
+ } catch {
29
+ ts = {};
30
+ }
31
+ const pr = prov.add("observed", p, "tsconfig-extractor", ["der:" + compRef + ":/languages", "rel:pending"]);
32
+
33
+ // Project references: an explicit build-time dependency between units.
34
+ for (const refPath of (ts.references || [])) {
35
+ const targetDir = path.posix.normalize(path.posix.join(unit.root, refPath));
36
+ const target = ctx.unitForRoot(targetDir);
37
+ if (!target || !target.component) continue;
38
+ ctx.addEdge("buildDependsOn", compRef, target.component.ref, { via: "tsconfig-references" }, [pr], "DERIVED");
39
+ }
40
+ derived.tsconfig = { composite: Boolean(ts.compilerOptions?.composite), references: (ts.references || []).length };
41
+
42
+ // Route conventions of the file-based router ecosystem: a route file is a
43
+ // real HTTP surface and must be visible to the query surface.
44
+ const routes = [];
45
+ for (const base of ["src/app", "app", "src/pages", "pages"]) {
46
+ if (!repo.isDir(unit.root + "/" + base)) continue;
47
+ for (const f of repo.walk(unit.root + "/" + base)) {
48
+ const routePath = routeFor(unit.root, base, f);
49
+ if (!routePath) continue;
50
+ const rpr = prov.add("observed", f, "route-extractor", ["der:" + compRef + ":/routes"]);
51
+ routes.push({ path: f, route: routePath, provenanceIds: null, _provs: new Set([rpr]) });
52
+ }
53
+ }
54
+ if (routes.length) {
55
+ routes.sort((a, b) => (a.route < b.route ? -1 : 1));
56
+ derived.routes = routes;
57
+ // Published on the unit so other adapters can resolve a scheduled
58
+ // trigger to the component that actually serves the route.
59
+ unit.routes = routes;
60
+ }
61
+ return derived;
62
+ }
63
+ }
64
+
65
+ function routeFor(root, base, file) {
66
+ const rel = file.slice((root + "/" + base).length + 1);
67
+ if (base.endsWith("app")) {
68
+ if (!rel.endsWith("/route.ts") && !rel.endsWith("/route.tsx") && !rel.endsWith("/route.js")) return null;
69
+ const segs = rel.split("/").slice(0, -1);
70
+ if (!segs.length) return "/";
71
+ return "/" + segs.map((s) => (s.startsWith("(") && s.endsWith(")") ? "" : s.replace(/^\[\.\.\.(.+)\]$/, ":$1*").replace(/^\[(.+)\]$/, ":$1"))).join("/");
72
+ }
73
+ if (!rel.startsWith("api/") || !rel.endsWith(".ts")) return null;
74
+ return "/" + rel.slice(4).replace(/\.ts$/, "");
75
+ }
76
+
77
+ // tsconfig allows comments and trailing commas; strip them before JSON.parse.
78
+ function parseTsconfig(repo, p) {
79
+ let text = repo.readText(p);
80
+ text = text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'\\])\/\/.*$/gm, "$1");
81
+ text = text.replace(/,(\s*[}\]])/g, "$1");
82
+ return JSON.parse(text);
83
+ }
84
+
85
+ export { parseYamlDocuments };
@@ -0,0 +1,206 @@
1
+ // Wrangler adapter (Cloudflare Workers / Pages).
2
+ //
3
+ // This adapter is where the most expensive lesson of the reference project
4
+ // lives. A committed deployment manifest is a *declared* schedule and a
5
+ // *declared* binding set. It is DERIVED evidence about what the repository
6
+ // intends, and nothing more. If a live scheduler disagrees — an hourly trigger
7
+ // with hour-3 gating where the manifest says "daily" — the disagreement has to
8
+ // reach the graph as a conflict, which only happens if the manifest claim is
9
+ // emitted as a fact on the same key the observation uses. So: no manifest value
10
+ // is ever reported as deployed truth.
11
+ import { Adapter } from "./registry.mjs";
12
+ import { parse as parseToml } from "../core/toml.mjs";
13
+ import { sourceFiles } from "../core/sourcescan.mjs";
14
+
15
+ const MANIFESTS = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"];
16
+
17
+ export class WranglerAdapter extends Adapter {
18
+ static adapterName = "wrangler";
19
+ constructor() {
20
+ super({ name: "wrangler", version: "1.0.0", kind: "unit-marker" });
21
+ }
22
+ detect(ctx, root) {
23
+ for (const n of MANIFESTS) {
24
+ if (ctx.repo.exists(root + "/" + n)) return { path: root + "/" + n, shape: "deployable" };
25
+ }
26
+ return null;
27
+ }
28
+ extract(ctx, unit) {
29
+ const { repo, prov, compRef } = ctx;
30
+ const file = MANIFESTS.map((n) => unit.root + "/" + n).find((p) => repo.exists(p));
31
+ if (!file) return {};
32
+ let toml;
33
+ try {
34
+ toml = file.endsWith(".toml") ? parseToml(repo.readText(file), file) : stripJsonc(repo.readText(file));
35
+ } catch {
36
+ return {};
37
+ }
38
+ const derived = { runtimes: new Set(["cloudflare-workers"]) };
39
+ if (toml.name) unit.platformName = String(toml.name);
40
+ const pr = prov.add("observed", file, "platform-manifest-extractor", ["der:" + compRef + ":/runtimes", "der:" + compRef + ":/bindings"]);
41
+
42
+ const bindings = [];
43
+ // The manifest makes ONE claim about the binding set, so the graph records
44
+ // one fact. Emitting a fact per binding would make a manifest that declares
45
+ // two bindings look like a contradiction against a runtime that reports one.
46
+ const bindingKinds = new Set();
47
+ const bindingNames = new Set();
48
+ const pushBinding = (kind, locator) => {
49
+ bindingKinds.add(kind);
50
+ if (locator && locator.binding) bindingNames.add(locator.binding);
51
+ bindings.push({ kind, locator, resolution: "resolved", provenanceIds: null, _provs: new Set([pr]) });
52
+ };
53
+ for (const kv of toml.kv_namespaces || []) pushBinding("kv", { binding: String(kv.binding || "") });
54
+ for (const d1 of toml.d1_databases || []) pushBinding("d1", { binding: String(d1.binding || "") });
55
+ for (const r2 of toml.r2_buckets || []) pushBinding("r2", { binding: String(r2.binding || "") });
56
+ for (const dofs of toml.durable_objects?.bindings || []) pushBinding("durable-object", { binding: String(dofs.name || "") });
57
+ for (const em of toml.send_email || []) pushBinding("email-routing", { binding: String(em.name || em.binding || "") });
58
+ for (const q of toml.queues?.producers || []) pushBinding("queue-producer", { binding: String(q.binding || "") });
59
+ for (const q of toml.queues?.consumers || []) pushBinding("queue-consumer", { binding: String(q.queue || q.binding || "") });
60
+ if (bindings.length) derived.bindings = bindings;
61
+
62
+ // Service bindings: a platform-level runtime call. Resolved in a second
63
+ // pass once every unit's platform name is known.
64
+ const pending = [];
65
+ for (const svc of toml.services || []) {
66
+ pushBinding("service-binding", { binding: String(svc.binding || ""), service: String(svc.service || "") });
67
+ pending.push({ binding: String(svc.binding || ""), service: String(svc.service || "") });
68
+ }
69
+ if (pending.length) {
70
+ derived.serviceBindings = pending;
71
+ ctx.deferSecondPass((pass) => {
72
+ for (const sb of pending) {
73
+ const target = pass.byPlatformName.get(sb.service);
74
+ if (target && target.component) {
75
+ const epr = prov.add("observed", file, "service-binding-extractor", ["rel:pending"]);
76
+ ctx.addEdge("runtimeCalls", compRef, target.component.ref,
77
+ { transport: "service-binding", contractRef: file }, [epr], "DERIVED");
78
+ } else {
79
+ ctx.diagnostics.push({
80
+ severity: "warning",
81
+ code: "AGENTDOC_SERVICE_BINDING_UNRESOLVED",
82
+ subject: compRef,
83
+ refs: [compRef],
84
+ message: "service binding '" + sb.service + "' declared in " + file + " does not match any discovered component",
85
+ paths: [file],
86
+ });
87
+ }
88
+ }
89
+ });
90
+ }
91
+
92
+ if (bindingNames.size) {
93
+ ctx.addFact(compRef, "binding.declared", [...bindingNames].sort(), {
94
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
95
+ semantics: "the binding names the committed manifest declares for this component",
96
+ });
97
+ }
98
+ if (bindingKinds.size) {
99
+ ctx.addFact(compRef, "binding.kind", [...bindingKinds].sort(), {
100
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
101
+ semantics: "the kinds of platform binding the committed manifest declares for this component",
102
+ });
103
+ }
104
+
105
+ // Scheduled triggers. Emitted as a DERIVED fact on schedule.cron, which is
106
+ // the same key a scheduler observation uses, so the two can be compared.
107
+ const crons = ((toml.triggers && toml.triggers.crons) || []).map(String).sort();
108
+ if (crons.length) {
109
+ derived.schedules = crons;
110
+ ctx.addFact(compRef, "schedule.cron", crons.join(","), {
111
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
112
+ semantics: "cron expressions the committed deployment manifest declares for this component",
113
+ });
114
+ ctx.addFact(compRef, "schedule.enabled", true, {
115
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
116
+ semantics: "the manifest declares a trigger, i.e. the job is intended to be enabled",
117
+ });
118
+ }
119
+
120
+ // Job handlers: a cron URL variable names the endpoint a trigger calls.
121
+ // Again one claim, not one per variable.
122
+ const vars = toml.vars || {};
123
+ const declaredTargets = [];
124
+ for (const [varName, url] of Object.entries(vars)) {
125
+ if (typeof url !== "string") continue;
126
+ const m = /^https?:\/\/[^/]+(\/[^\s]*)$/.exec(url);
127
+ if (!m) continue;
128
+ declaredTargets.push({ var: varName, route: m[1] });
129
+ }
130
+ if (declaredTargets.length) {
131
+ derived.scheduleTargets = declaredTargets.slice().sort((a, b) => (a.route < b.route ? -1 : 1));
132
+ ctx.addFact(compRef, "schedule.target", [...new Set(declaredTargets.map((t) => t.route))].sort(), {
133
+ evidenceClass: "DERIVED", confidence: "deterministic", provRecs: [pr],
134
+ semantics: "endpoints the committed manifest points scheduled triggers at",
135
+ });
136
+ // A schedule that points at a route this repository implements is a
137
+ // `schedules` relation. Without it, "who does this trigger call?" has no
138
+ // answer and an agent debugging a missed job has nothing to follow.
139
+ for (const t of declaredTargets) {
140
+ ctx.deferSecondPass(() => {
141
+ const owner = findRouteOwner(ctx, t.route, compRef);
142
+ if (owner) {
143
+ const rpr = prov.add("observed", file, "schedule-extractor", ["rel:pending"]);
144
+ const tpr = prov.add("observed", owner.file, "schedule-extractor", ["rel:pending"]);
145
+ ctx.addEdge("schedules", compRef, owner.ref, { via: "declared-trigger-target", route: t.route }, [rpr, tpr], "DERIVED");
146
+ } else {
147
+ ctx.diagnostics.push({
148
+ severity: "warning",
149
+ code: "AGENTDOC_SCHEDULE_UNRESOLVED",
150
+ subject: compRef,
151
+ refs: [compRef],
152
+ message: "scheduled trigger target '" + t.route + "' declared in " + file + " does not resolve to a route in any discovered component",
153
+ paths: [file],
154
+ });
155
+ }
156
+ });
157
+ }
158
+ }
159
+ return derived;
160
+ }
161
+ }
162
+
163
+ // Resolve an absolute route path to the component that *serves* it.
164
+ //
165
+ // A route literal appears in more places than the component that owns it: every
166
+ // generated OpenAPI client embeds the path it calls. So candidates are ranked
167
+ // rather than counted, and only an unambiguous winner is used:
168
+ //
169
+ // 2 a file-based route whose path is this route
170
+ // 1 a route literal in hand-written source
171
+ // 0 a route literal in generated client code
172
+ //
173
+ // A tie at the top score is a genuine ambiguity and yields no relation.
174
+ function findRouteOwner(ctx, route, selfRef) {
175
+ const generated = (ctx.cfg.discovery.generatedClientPatterns || ["/gen/", ".gen.", "/generated/", "/generated/"]).map(
176
+ (p) => new RegExp(escapeRe(p))
177
+ );
178
+ const isGenerated = (f) => generated.some((re) => re.test(f));
179
+ const candidates = [];
180
+ for (const unit of ctx.discovery.eligible) {
181
+ if (!unit.component || unit.component.ref === selfRef) continue;
182
+ const viaPath = (unit.routes || []).find((r) => r.route === route);
183
+ if (viaPath) { candidates.push({ ref: unit.component.ref, file: viaPath.path, score: 2 }); continue; }
184
+ for (const f of sourceFiles(ctx.repo, unit.root, { test: false })) {
185
+ const text = ctx.repo.readText(f);
186
+ if (!text.includes(route)) continue;
187
+ if (!new RegExp("[\"'\`]" + escapeRe(route) + "[\"'\`]").test(text)) continue;
188
+ candidates.push({ ref: unit.component.ref, file: f, score: isGenerated(f) ? 0 : 1 });
189
+ break;
190
+ }
191
+ }
192
+ if (!candidates.length) return null;
193
+ const best = Math.max(...candidates.map((c) => c.score));
194
+ const winners = candidates.filter((c) => c.score === best);
195
+ const distinct = new Set(winners.map((c) => c.ref));
196
+ if (distinct.size !== 1) return null;
197
+ return winners[0];
198
+ }
199
+
200
+ function escapeRe(s) {
201
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
202
+ }
203
+
204
+ function stripJsonc(text) {
205
+ return JSON.parse(text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'\\])\/\/.*$/gm, "$1").replace(/,(\s*[}\]])/g, "$1"));
206
+ }