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,322 @@
1
+ #!/usr/bin/env node
2
+ // CREATE-mode scaffolding.
3
+ //
4
+ // Generates a descriptor skeleton from what the repository can actually prove.
5
+ // The generation rules are deliberately conservative:
6
+ // - a component descriptor is written only for a discovered unit with a name
7
+ // the unit itself supplies (package name, module path, or directory name);
8
+ // - a description is a template sentence naming the artifact evidence, never
9
+ // an invented product claim;
10
+ // - a placement that cannot be inferred is left out, and the compiler will
11
+ // report it as a question rather than the scaffolder guessing.
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { AgentDocError, CODES } from "./codes.mjs";
16
+ import { Repo } from "./fsx.mjs";
17
+ import { classifyContractFile } from "./contracts.mjs";
18
+
19
+ const TEMPLATES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "templates");
20
+
21
+ function yamlStr(s) {
22
+ return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
23
+ }
24
+
25
+ function kebab(s) {
26
+ return String(s)
27
+ .replace(/^@/, "")
28
+ .replace(/[_\s]+/g, "-")
29
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
30
+ .replace(/[^A-Za-z0-9-]+/g, "-")
31
+ .replace(/-+/g, "-")
32
+ .replace(/^-+|-+$/g, "")
33
+ .toLowerCase()
34
+ .slice(0, 63) || "component";
35
+ }
36
+
37
+ function sentence(s) {
38
+ const t = String(s).replace(/\s+/g, " ").trim();
39
+ if (!t) return "TODO: describe this in one present-tense sentence.";
40
+ return /[.!?]$/.test(t) ? t : t + ".";
41
+ }
42
+
43
+ export function installTemplates(repo, cfg, { write, root, force = false, eligible = null } = {}) {
44
+ const written = [];
45
+ const skipped = [];
46
+ const targetRoot = root || (repo ? repo.root : process.cwd());
47
+ const r = repo || new Repo(targetRoot, { noGit: false });
48
+
49
+ const put = (rel, content) => {
50
+ const abs = path.join(targetRoot, rel);
51
+ if (fs.existsSync(abs) && !force) { skipped.push(rel); return; }
52
+ if (!write) { written.push(rel + " (dry run)"); return; }
53
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
54
+ fs.writeFileSync(abs, content.endsWith("\n") ? content : content + "\n");
55
+ written.push(rel);
56
+ };
57
+
58
+ // ── configuration ───────────────────────────────────────────────────────
59
+ const config = buildConfig(scanLayout(r));
60
+ put("agentdoc/agentdoc.config.yaml", config);
61
+ put("agentdoc/journeys.yaml", "schemaVersion: agentdoc.dev/journeys/v1\njourneys: []\n");
62
+ for (const f of ["domains.yaml", "systems.yaml", "apis.yaml", "resources.yaml"]) {
63
+ put("agentdoc/" + f, "");
64
+ }
65
+ put("agentdoc/observations/README.md", OBSERVATIONS_README);
66
+
67
+ // ── documentation hierarchy ──────────────────────────────────────────────
68
+ for (const f of ["PRODUCT.md", "ARCHITECTURE.md", "CONSTRAINTS.md"]) {
69
+ const tpl = fs.readFileSync(path.join(TEMPLATES_DIR, f), "utf8");
70
+ put("docs/" + f, tpl);
71
+ }
72
+ put("docs/adr/README.md", fs.readFileSync(path.join(TEMPLATES_DIR, "ADR.md"), "utf8"));
73
+ put(".github/workflows/agentdoc.yml", fs.readFileSync(path.join(TEMPLATES_DIR, "ci.yml"), "utf8"));
74
+
75
+ // ── per-unit component descriptors ──────────────────────────────────────
76
+ if (cfg) {
77
+ // When the caller ran the compiler's discovery first, write for the units
78
+ // the compiler itself counts as eligible — adapter-discovered roots
79
+ // (go.work members, pnpm workspace packages, supplementalRoots) may have
80
+ // no marker file the layout scan would find on its own.
81
+ const units = discoverUnitNames(r, cfg, eligible);
82
+ for (const u of units) {
83
+ put(u.root + "/" + cfg.discovery.componentDescriptorName, componentDescriptor(u));
84
+ }
85
+ }
86
+ return { written, skipped, note: write ? "files written" : "dry run: pass --write to apply" };
87
+ }
88
+
89
+ // Layout inference for CREATE mode: which directories look like unit roots.
90
+ // Uses only file-presence heuristics that any repository can answer.
91
+ function scanLayout(repo) {
92
+ const candidates = new Map();
93
+ const note = (root, kind) => {
94
+ if (!candidates.has(root)) candidates.set(root, { root, kinds: new Set(), evidence: [] });
95
+ candidates.get(root).kinds.add(kind);
96
+ candidates.get(root).evidence.push(kind);
97
+ };
98
+ const skip = new Set(["node_modules", ".git", "dist", "build", "vendor", "target", "agentdoc", "docs", "scripts", ".github", "test", "tests", "examples", "example"]);
99
+ const walk = (dir, depth) => {
100
+ if (depth > 3) return;
101
+ let entries;
102
+ try { entries = fs.readdirSync(path.join(repo.root, dir), { withFileTypes: true }); } catch { return; }
103
+ for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
104
+ if (e.isSymbolicLink() || skip.has(e.name)) continue;
105
+ const rel = dir ? dir + "/" + e.name : e.name;
106
+ if (e.isDirectory()) {
107
+ if (e.name.startsWith(".")) continue;
108
+ walk(rel, depth + 1);
109
+ continue;
110
+ }
111
+ // Parent directory of the manifest. Parenthesised deliberately: without
112
+ // the brackets, `-"/" + e.name` binds first and slice(0, NaN) silently
113
+ // yields the empty string, which then crashes the layout analysis.
114
+ const parent = () => rel.slice(0, -("/" + e.name).length);
115
+ if (e.name === "package.json") note(parent(), "node");
116
+ if (e.name === "go.mod") note(parent(), "go");
117
+ if (e.name === "Dockerfile") note(parent(), "docker");
118
+ if (/^wrangler\.(toml|json|jsonc)$/.test(e.name)) note(parent(), "platform");
119
+ if (e.name === "go.mod") note(rel.slice(0, -"/go.mod".length), "go");
120
+ }
121
+ };
122
+ walk("", 0);
123
+ return [...candidates.values()].sort((a, b) => (a.root < b.root ? -1 : 1));
124
+ }
125
+
126
+ function discoverUnitNames(repo, cfg, eligible = null) {
127
+ const out = [];
128
+ const nameFor = (root) => {
129
+ const pkg = root + "/package.json";
130
+ if (repo.exists(pkg)) {
131
+ try {
132
+ const j = repo.readJson(pkg);
133
+ if (typeof j.name === "string" && j.name) return kebab(j.name);
134
+ } catch { /* fall through */ }
135
+ }
136
+ const gomod = root + "/go.mod";
137
+ if (repo.exists(gomod)) {
138
+ const m = /^module\s+(\S+)/m.exec(repo.readText(gomod));
139
+ if (m) return kebab(m[1].split("/").pop());
140
+ }
141
+ return kebab(path.basename(root));
142
+ };
143
+ const seen = new Set();
144
+ const seenRoots = new Set();
145
+ // Union, not replacement: compiler-eligible units (adapter roots,
146
+ // supplementalRoots) cover what markers cannot see, while the layout scan
147
+ // still covers marker-bearing dirs that no adapter claims.
148
+ const units = [
149
+ ...(eligible || []).map((u) => ({
150
+ root: u.root,
151
+ kinds: (u.markers || []).map((m) => m.adapter).sort(),
152
+ deployable: Boolean(u.deployable),
153
+ importable: Boolean(u.importable),
154
+ })),
155
+ ...scanLayout(repo).map((u) => ({ root: u.root, kinds: [...u.kinds].sort(), deployable: false, importable: false })),
156
+ ];
157
+ for (const u of units) {
158
+ if (u.root === "." || !u.root || seenRoots.has(u.root)) continue;
159
+ seenRoots.add(u.root);
160
+ const name = nameFor(u.root);
161
+ if (seen.has(name)) continue;
162
+ seen.add(name);
163
+ out.push({ ...u, name });
164
+ }
165
+ void cfg;
166
+ return out;
167
+ }
168
+
169
+ function componentDescriptor(u) {
170
+ const deployable = u.deployable || u.kinds.some((k) => k === "docker" || k === "platform");
171
+ const importable = u.importable || u.kinds.some((k) => k === "node" || k === "go");
172
+ // "library" and "service" both have to be corroborated by artifact evidence
173
+ // (AGENTDOC_ARTIFACT_TYPE); a unit the compiler only knows from configuration
174
+ // shows neither, so claiming a concrete type would manufacture an error.
175
+ const type = deployable ? "service" : importable ? "library" : "other";
176
+ return [
177
+ "apiVersion: agentdoc.dev/v1",
178
+ "kind: Component",
179
+ "metadata:",
180
+ " name: " + u.name,
181
+ " description: " + yamlStr(sentence("TODO: " + u.name + " (" + (u.kinds.length ? u.kinds.join("/") : "configured") + " at " + u.root + ") — state its single current responsibility")),
182
+ "spec:",
183
+ " type: " + type,
184
+ " # Choose exactly one of the three lines below. Uncomment one.",
185
+ " # system: <system-name>",
186
+ " # domain: <domain-name>",
187
+ " # placementRationale: " + yamlStr(sentence("TODO: explain why this component belongs to no system or domain")),
188
+ "",
189
+ ].join("\n");
190
+ }
191
+
192
+ // Which adapter a discovered manifest marker implies. Project-agnostic: this
193
+ // maps a technology to its extractor, never a folder name to a component.
194
+ const ADAPTER_FOR_MARKER = {
195
+ node: "node",
196
+ go: "go",
197
+ docker: "dockerfile",
198
+ platform: "wrangler",
199
+ };
200
+
201
+ function buildConfig(layout) {
202
+ const roots = layout.map((l) => l.root);
203
+ const groups = new Map();
204
+ for (const r of roots) {
205
+ const head = r.split("/")[0];
206
+ if (!groups.has(head)) groups.set(head, []);
207
+ groups.get(head).push(r);
208
+ }
209
+ // A manifest at the repository root yields an empty head; emitting
210
+ // "/agentdoc.yaml" would be an absolute glob, which the loader rejects.
211
+ const componentGlobs = [...groups.entries()]
212
+ .filter(([head]) => head.length > 0)
213
+ .sort((a, b) => (a[0] < b[0] ? -1 : 1))
214
+ .map(([head, list]) => (list.length === 1 && list[0] === head ? head + "/agentdoc.yaml" : head + "/**/agentdoc.yaml"));
215
+ if (!componentGlobs.length) componentGlobs.push("*/**/agentdoc.yaml");
216
+
217
+ const adapters = new Set(["generic"]);
218
+ for (const l of layout) {
219
+ for (const k of l.kinds) {
220
+ if (ADAPTER_FOR_MARKER[k]) adapters.add(ADAPTER_FOR_MARKER[k]);
221
+ }
222
+ }
223
+ adapters.add("database");
224
+ adapters.add("openapi");
225
+ adapters.add("events");
226
+ adapters.add("github-actions");
227
+
228
+ // Candidate contract roots, filtered to those that actually exist. These are
229
+ // only suggestions: the engine has no opinion about a folder name.
230
+ const contractRoots = ["contracts", "api", "specs", "proto", "openapi", "schemas"].filter((r) => groups.has(r) || r === "");
231
+ return [
232
+ "# agentdoc configuration — the only project-specific file in the system.",
233
+ "# Everything the engine does is derived from this plus the repository.",
234
+ "apiVersion: agentdoc.dev/config/v1",
235
+ "# namespace prefixes every entity ref (component:<namespace>/<name>).",
236
+ "# 'default' is a placeholder — pick the name this repository calls itself.",
237
+ "namespace: default",
238
+ "",
239
+ "discovery:",
240
+ " componentDescriptorName: agentdoc.yaml",
241
+ " componentDescriptors:",
242
+ // Globs are quoted: an unquoted leading "*" is an alias in strict YAML,
243
+ // and a configuration the compiler cannot parse is worse than none.
244
+ ...componentGlobs.map((g) => " - " + yamlStr(g)),
245
+ " centralDescriptors:",
246
+ " - agentdoc/domains.yaml",
247
+ " - agentdoc/systems.yaml",
248
+ " - agentdoc/apis.yaml",
249
+ " - agentdoc/resources.yaml",
250
+ " journeyDefinitions: agentdoc/journeys.yaml",
251
+ ...(contractRoots.filter((r) => groups.has(r)).length
252
+ ? [" contractRoots:", ...contractRoots.filter((r) => groups.has(r)).map((r) => " - " + r)]
253
+ : [" # contractRoots: [] # uncomment once you have a canonical contract directory"]),
254
+ " # eventPatternPrefixes:",
255
+ " # - acme.",
256
+ "",
257
+ "adapters:",
258
+ ...[...adapters].sort().map((a) => " - " + a),
259
+ "",
260
+ "authority:",
261
+ " # No default global precedence exists. Every fact kind that can disagree",
262
+ " # across evidence classes needs an explicit rule, or it stays unresolved.",
263
+ " #",
264
+ " # The three rules below are COMMENTED OUT ON PURPOSE. An authority rule is a",
265
+ " # governance decision about which class of evidence wins, and a scaffolder is",
266
+ " # not entitled to make it for you. Uncomment the ones you can justify, and",
267
+ " # write the rationale in your own words:",
268
+ " #",
269
+ " # rules:",
270
+ " # - id: deployed-schedule-is-runtime-truth",
271
+ " # keyPattern: ' schedule\\\\.(cron|enabled|target)$'",
272
+ " # elect: [OBSERVED_RUNTIME]",
273
+ ' # rationale: "Why this class is authoritative for this key."',
274
+ ' # reviewWhen: "What has to change for this rule to be re-examined."',
275
+ " rules: []",
276
+ "",
277
+ "reviewedOverrides: []",
278
+ "warningAcceptances: []",
279
+ "",
280
+ "output:",
281
+ " graph: .agentdoc/graph.json",
282
+ " observations: agentdoc/observations",
283
+ "",
284
+ "docs:",
285
+ " product: docs/PRODUCT.md",
286
+ " architecture: docs/ARCHITECTURE.md",
287
+ " constraints: docs/CONSTRAINTS.md",
288
+ " adrDir: docs/adr",
289
+ "",
290
+ ].join("\n");
291
+ }
292
+
293
+ const OBSERVATIONS_README = `# Runtime observations
294
+
295
+ An ObservationSet records what an authoritative external system actually said,
296
+ at a known time, with a durable reference to the raw evidence.
297
+
298
+ ## Rules
299
+
300
+ 1. Never put a secret, token or credentialed URL in an observation, an evidence
301
+ bundle, or anywhere else in this directory. Run \`agentdoc validate\`; it
302
+ fails on secret-shaped input.
303
+ 2. Every fact needs a \`subject\` (an entity ref that exists) and a \`key\`
304
+ (see "Fact keys" in the AgentDoc specification,
305
+ docs/SPEC.md of https://github.com/lbframe/agent-doc-system). Two
306
+ observation sets must not use the same key on the same subject.
307
+ 3. \`evidenceBundle\` must be a committed, repository-relative file. A temp
308
+ path, a console URL with a session token, or a screenshot filename is not
309
+ evidence.
310
+ 4. \`maxAgeDays\` is a promise. When the observation is older than that, every
311
+ gate fails closed and says so. Re-observe, or restate the fact as authored
312
+ intent if the divergence is intended.
313
+
314
+ ## Shape
315
+
316
+ See the \`observation.yaml\` template shipped with the agentdoc CLI
317
+ (templates/observation.yaml in the installed package).
318
+ `;
319
+
320
+ export function templatePaths() {
321
+ return { dir: TEMPLATES_DIR };
322
+ }
@@ -0,0 +1,92 @@
1
+ // Secret material must never reach the compiled graph, a report, a log line,
2
+ // or a prompt. Two layers:
3
+ //
4
+ // 1. Source scanning — every authored/observed document is scanned before it
5
+ // can influence any output.
6
+ // 2. Output scanning — the serialized graph, every report and every query
7
+ // payload is scanned again, because derivations and extractor heuristics can
8
+ // synthesize a secret-shaped string that no source scan would have caught.
9
+ import { AgentDocError, CODES } from "./codes.mjs";
10
+
11
+ const PATTERNS = [
12
+ { re: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/, why: "private key material" },
13
+ { re: /\bAKIA[0-9A-Z]{16}\b/, why: "AWS-style access key id" },
14
+ { re: /\bASIA[0-9A-Z]{16}\b/, why: "AWS-style temporary access key id" },
15
+ { re: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/, why: "GitHub token" },
16
+ { re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/, why: "GitHub fine-grained token" },
17
+ { re: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/, why: "Slack token" },
18
+ { re: /\bsk-[A-Za-z0-9]{20,}\b/, why: "provider secret key" },
19
+ { re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/, why: "JWT-shaped token" },
20
+ { re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/@]+:[^\s/@]+@/, why: "URL with embedded credentials" },
21
+ {
22
+ re: /\b(?:secret|password|passwd|passphrase|token|api[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token)\b\s*[:=]\s*["']?[^\s"',;]{8,}/i,
23
+ why: "credential assignment",
24
+ },
25
+ { re: /\bBearer\s+[A-Za-z0-9._~+/-]{20,}=*/, why: "bearer token" },
26
+ { re: /\b(?:curl|wget)\b[^\n]*\s-[a-z]*u\s+\S+:\S+/i, why: "inline credentials on a command line" },
27
+ { re: /\bAuthorization\s*[:=]\s*["']?[^\s"']{8,}/i, why: "authorization header value" },
28
+ { re: /\baws_secret_access_key\b\s+\S{16,}/i, why: "AWS secret access key" },
29
+ { re: /\b(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\b\s+["']?[A-Za-z0-9_\-]{16,}/i, why: "credential passed as an argument" },
30
+ ];
31
+
32
+ function scanString(value, where) {
33
+ // Values this system already redacted are not secrets. Removing the marker
34
+ // before matching stops the output scanner from refusing to emit its own
35
+ // redaction, which would make a redacted command unusable.
36
+ const v = String(value).replace(/\[REDACTED[^\]]*\]/g, "");
37
+ for (const { re, why } of PATTERNS) {
38
+ if (re.test(v)) {
39
+ throw new AgentDocError(CODES.SECRET, "secret-like value (" + why + ") at " + where + ": agentdoc sources and outputs must never contain credentials", {});
40
+ }
41
+ }
42
+ }
43
+
44
+ // Recursive scan of an in-memory document.
45
+ export function scanForSecrets(node, path, pointer = "") {
46
+ if (node == null) return;
47
+ if (typeof node === "string") return scanString(node, pointer || "/");
48
+ if (typeof node !== "object") return;
49
+ if (Array.isArray(node)) {
50
+ node.forEach((item, i) => scanForSecrets(item, path, pointer + "/" + i));
51
+ return;
52
+ }
53
+ for (const [k, v] of Object.entries(node)) {
54
+ scanForSecrets(v, path, pointer + "/" + k.replace(/~/g, "~0").replace(/\//g, "~1"));
55
+ }
56
+ }
57
+
58
+ // Scan serialized text (a graph, a report, a query payload). Throws on the
59
+ // first hit; the caller decides whether that is fatal.
60
+ export function assertNoSecretsInText(text, what) {
61
+ const scrubbed = String(text).replace(/\[REDACTED[^\]]*\]/g, "");
62
+ for (const { re, why } of PATTERNS) {
63
+ const m = re.exec(scrubbed);
64
+ if (m) {
65
+ throw new AgentDocError(
66
+ CODES.SECRET,
67
+ "refusing to emit " + what + ": secret-like value (" + why + ") near offset " + m.index
68
+ );
69
+ }
70
+ }
71
+ }
72
+
73
+ // Verification commands are a routing index, not a credential transport. Keep
74
+ // the runnable shape; strip any inline assignment or DSN password.
75
+ const SECRET_NAME = /(?:SECRET|TOKEN|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|PRIVATE_?KEY|CREDENTIAL|ACCESS_?KEY)/i;
76
+
77
+ export function redactCommand(command) {
78
+ return String(command)
79
+ // Assignment form. The name is matched structurally and then tested, rather
80
+ // than by a single backtracking pattern: "API_KEY" must be recognised as
81
+ // an API key, which a greedy name group would swallow whole.
82
+ .replace(/\b([A-Za-z_][A-Za-z0-9_]*)=([^\s;"'`]+)/g, (m, name, value) =>
83
+ SECRET_NAME.test(name) ? name + "=[REDACTED]" : m)
84
+ .replace(/\b((?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp|https?):\/\/)[^:@/\s]+:[^@/\s]+@/gi,
85
+ "$1[REDACTED]@")
86
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/g, "$1[REDACTED]")
87
+ // `curl -u user:pass` and `<credential-name> <value>` as separate arguments.
88
+ .replace(/(\s-[a-zA-Z]*u\s+)(\S+):(\S+)/g, "$1$2:[REDACTED]")
89
+ .replace(/\b(aws_secret_access_key|aws_access_key_id)(\s+)([A-Za-z0-9/+=]{12,})/gi, "$1$2[REDACTED]")
90
+ .replace(/\b(Authorization)(\s*[:=]\s*)(\S{8,})/gi, "$1$2[REDACTED]")
91
+ .replace(/\b(api[_-]?key|apikey|access[_-]?token|auth[_-]?token)(\s+)(["']?)([A-Za-z0-9_-]{16,})/gi, "$1$2$3[REDACTED]");
92
+ }
@@ -0,0 +1,88 @@
1
+ // Source scanning primitives shared by adapters.
2
+ //
3
+ // The extraction of quoted module specifiers is language-neutral: a bare
4
+ // identifier in quotes that is not a relative path is a candidate dependency
5
+ // reference in practically every language. Deciding whether that reference
6
+ // resolves to a sibling unit is the adapter's job, because only the adapter
7
+ // knows the namespace convention.
8
+ import path from "node:path";
9
+ import fs from "node:fs";
10
+
11
+ const SPECIFIER_RE = /["'`]([A-Za-z@][A-Za-z0-9@._/+-]*(?:\.[A-Za-z0-9@._/+-]+)*(?:\/[A-Za-z0-9@._+*-]+)*)["'`]/g;
12
+
13
+ export function importSpecifiers(text) {
14
+ const out = new Set();
15
+ for (const m of text.matchAll(SPECIFIER_RE)) {
16
+ const s = m[1];
17
+ if (s.startsWith(".") || s.startsWith("/")) continue;
18
+ if (s.length < 2) continue;
19
+ out.add(s);
20
+ }
21
+ return out;
22
+ }
23
+
24
+ export function sourceFiles(repo, root, { test = false, extensions } = {}) {
25
+ const TEST_PATH = /(^|\/)(tests?|__tests__|spec)\//;
26
+ const TEST_FILE = /\.(test|spec)\./;
27
+ const DEFAULT_EXT = /\.(ts|tsx|js|jsx|mts|cts|mjs|cjs|go|py|rb|java|kt|rs|ex|exs|cs|php|swift|scala|sh)$/;
28
+ const re = extensions || DEFAULT_EXT;
29
+ const out = [];
30
+ for (const f of repo.walk(root)) {
31
+ if (!re.test(f)) continue;
32
+ const isTest = TEST_PATH.test(f) || TEST_FILE.test(f) || /_test\.[a-z]+$/.test(f);
33
+ if (isTest !== test) continue;
34
+ out.push(f);
35
+ }
36
+ return out;
37
+ }
38
+
39
+ // Directory candidates that hold schema migrations or an ORM schema. Used to
40
+ // find "this component owns a database" evidence without assuming a framework.
41
+ export const MIGRATION_DIR_CANDIDATES = [
42
+ "migrations", "db/migrations", "database/migrations", "internal/db/migrations",
43
+ "internal/migrations", "src/db/migrations", "alembic/versions", "liquibase",
44
+ ];
45
+
46
+ // Any directory named "migrations" inside a unit, at bounded depth. Framework
47
+ // and language conventions move; the name of a migration directory has been
48
+ // stable for far longer than its location.
49
+ export function findMigrationDirs(repo, root, maxDepth = 5) {
50
+ const out = [];
51
+ // Absolute paths throughout: `root` is repository-relative, and resolving it
52
+ // against the process working directory would make the result depend on
53
+ // where the compiler was invoked from.
54
+ const base = repo.abs(root);
55
+ const walk = (dirAbs, depth) => {
56
+ if (depth > maxDepth) return;
57
+ let entries;
58
+ try {
59
+ entries = fs.readdirSync(dirAbs, { withFileTypes: true });
60
+ } catch {
61
+ return;
62
+ }
63
+ for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
64
+ if (!e.isDirectory() || e.isSymbolicLink()) continue;
65
+ if (["node_modules", ".git", "vendor", "target", "dist", "build", ".next", "__pycache__"].includes(e.name)) continue;
66
+ const abs = path.join(dirAbs, e.name);
67
+ if (e.name === "migrations" || e.name === "migration") {
68
+ out.push(path.relative(repo.root, abs).split(path.sep).join("/"));
69
+ continue;
70
+ }
71
+ walk(abs, depth + 1);
72
+ }
73
+ };
74
+ walk(base, 0);
75
+ return out.sort();
76
+ }
77
+
78
+ export function firstExistingFile(repo, root, names) {
79
+ for (const n of names) {
80
+ if (repo.exists(root + "/" + n)) return root + "/" + n;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ export function relativeTo(root, p) {
86
+ const r = path.posix.relative(root, p);
87
+ return r === "" ? "." : r;
88
+ }
package/core/toml.mjs ADDED
@@ -0,0 +1,163 @@
1
+ // Minimal TOML reader for platform deployment manifests.
2
+ //
3
+ // Only what a deployment manifest needs: tables, arrays of tables, key/value
4
+ // pairs, strings, integers, booleans, inline arrays and inline tables. Anything
5
+ // richer is rejected rather than guessed at, because a mis-parsed manifest
6
+ // would produce confident nonsense about deployed state.
7
+ import { AgentDocError, CODES } from "./codes.mjs";
8
+
9
+ export function parse(src, path = "") {
10
+ const root = {};
11
+ let cur = root;
12
+ const lines = String(src).split("\n");
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const line = stripComment(lines[i]).trim();
15
+ if (!line) continue;
16
+ const table = /^\[\[?([^\]]+)\]\]?$/.exec(line);
17
+ if (table) {
18
+ const isArray = line.startsWith("[[");
19
+ const keys = splitKeyPath(table[1]);
20
+ cur = root;
21
+ for (let k = 0; k < keys.length; k++) {
22
+ const key = keys[k];
23
+ const last = k === keys.length - 1;
24
+ if (last && isArray) {
25
+ if (!Array.isArray(cur[key])) cur[key] = [];
26
+ const obj = {};
27
+ cur[key].push(obj);
28
+ cur = obj;
29
+ } else {
30
+ if (cur[key] === undefined) cur[key] = {};
31
+ cur = cur[key];
32
+ }
33
+ }
34
+ continue;
35
+ }
36
+ const eq = findEquals(line);
37
+ if (eq < 0) {
38
+ throw new AgentDocError(CODES.YAML_PARSE, "unparseable TOML line " + (i + 1) + (path ? " in " + path : ""), { path, line: i + 1 });
39
+ }
40
+ const key = unquoteKey(line.slice(0, eq).trim());
41
+ let valueText = line.slice(eq + 1).trim();
42
+ // Multi-line arrays.
43
+ while (unbalanced(valueText) && i + 1 < lines.length) {
44
+ i++;
45
+ valueText += " " + stripComment(lines[i]).trim();
46
+ }
47
+ const keys = splitKeyPath(key);
48
+ let target = cur;
49
+ for (let k = 0; k < keys.length - 1; k++) {
50
+ if (target[keys[k]] === undefined) target[keys[k]] = {};
51
+ target = target[keys[k]];
52
+ }
53
+ target[keys[keys.length - 1]] = value(valueText, path, i + 1);
54
+ }
55
+ return root;
56
+ }
57
+
58
+ function unbalanced(s) {
59
+ let b = 0;
60
+ let q = null;
61
+ for (let i = 0; i < s.length; i++) {
62
+ const c = s[i];
63
+ if (q) { if (c === "\\") { i++; continue; } if (c === q) q = null; continue; }
64
+ if (c === '"' || c === "'") { q = c; continue; }
65
+ if (c === "[" || c === "{") b++;
66
+ if (c === "]" || c === "}") b--;
67
+ }
68
+ return b > 0;
69
+ }
70
+
71
+ function stripComment(s) {
72
+ let q = null;
73
+ for (let i = 0; i < s.length; i++) {
74
+ const c = s[i];
75
+ if (q) { if (c === "\\" && q === '"') { i++; continue; } if (c === q) q = null; continue; }
76
+ if (c === '"' || c === "'") { q = c; continue; }
77
+ if (c === "#") return s.slice(0, i);
78
+ }
79
+ return s;
80
+ }
81
+
82
+ function findEquals(line) {
83
+ let q = null;
84
+ for (let i = 0; i < line.length; i++) {
85
+ const c = line[i];
86
+ if (q) { if (c === "\\" && q === '"') { i++; continue; } if (c === q) q = null; continue; }
87
+ if (c === '"' || c === "'") { q = c; continue; }
88
+ if (c === "=") return i;
89
+ }
90
+ return -1;
91
+ }
92
+
93
+ function splitKeyPath(s) {
94
+ return s.split(".").map((k) => unquoteKey(k.trim()));
95
+ }
96
+
97
+ function unquoteKey(k) {
98
+ if ((k.startsWith('"') && k.endsWith('"')) || (k.startsWith("'") && k.endsWith("'"))) return k.slice(1, -1);
99
+ return k;
100
+ }
101
+
102
+ function value(t, path, line) {
103
+ if (t === "true") return true;
104
+ if (t === "false") return false;
105
+ if (/^".*"$/.test(t)) return JSON.parse(t.replace(/\\'/g, "'"));
106
+ if (/^'.*'$/.test(t)) return t.slice(1, -1);
107
+ if (/^-?\d+$/.test(t)) return Number(t);
108
+ if (/^-?\d+\.\d+$/.test(t)) return Number(t);
109
+ if (t.startsWith("[")) return parseArray(t, path, line);
110
+ if (t.startsWith("{")) return parseInlineTable(t, path, line);
111
+ if (t === "") return "";
112
+ return t;
113
+ }
114
+
115
+ function parseArray(t, path, line) {
116
+ const inner = t.slice(1, -1).trim();
117
+ if (!inner) return [];
118
+ const out = [];
119
+ let buf = "";
120
+ let q = null;
121
+ let depth = 0;
122
+ for (let i = 0; i < inner.length; i++) {
123
+ const c = inner[i];
124
+ if (q) { buf += c; if (c === "\\") { buf += inner[++i] || ""; continue; } if (c === q) q = null; continue; }
125
+ if (c === '"' || c === "'") { q = c; buf += c; continue; }
126
+ if (c === "[" || c === "{") { depth++; buf += c; continue; }
127
+ if (c === "]" || c === "}") { depth--; buf += c; continue; }
128
+ if (c === "," && depth === 0) { out.push(value(buf.trim(), path, line)); buf = ""; continue; }
129
+ buf += c;
130
+ }
131
+ if (buf.trim()) out.push(value(buf.trim(), path, line));
132
+ return out;
133
+ }
134
+
135
+ function parseInlineTable(t, path, line) {
136
+ const inner = t.slice(1, -1).trim();
137
+ const out = {};
138
+ if (!inner) return out;
139
+ for (const part of splitTop(inner)) {
140
+ const eq = findEquals(part);
141
+ if (eq < 0) continue;
142
+ out[unquoteKey(part.slice(0, eq).trim())] = value(part.slice(eq + 1).trim(), path, line);
143
+ }
144
+ return out;
145
+ }
146
+
147
+ function splitTop(s) {
148
+ const out = [];
149
+ let buf = "";
150
+ let q = null;
151
+ let depth = 0;
152
+ for (let i = 0; i < s.length; i++) {
153
+ const c = s[i];
154
+ if (q) { buf += c; if (c === "\\") { buf += s[++i] || ""; continue; } if (c === q) q = null; continue; }
155
+ if (c === '"' || c === "'") { q = c; buf += c; continue; }
156
+ if (c === "[" || c === "{") { depth++; buf += c; continue; }
157
+ if (c === "]" || c === "}") { depth--; buf += c; continue; }
158
+ if (c === "," && depth === 0) { out.push(buf.trim()); buf = ""; continue; }
159
+ buf += c;
160
+ }
161
+ if (buf.trim()) out.push(buf.trim());
162
+ return out;
163
+ }