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.
- package/INSTALL_FOR_AGENTS.md +60 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/adapters/ansible.mjs +306 -0
- package/adapters/cache.mjs +27 -0
- package/adapters/contract-consumers.mjs +56 -0
- package/adapters/database.mjs +116 -0
- package/adapters/deploy-config.mjs +34 -0
- package/adapters/dockerfile.mjs +26 -0
- package/adapters/events.mjs +145 -0
- package/adapters/generic.mjs +86 -0
- package/adapters/github-actions.mjs +106 -0
- package/adapters/go.mjs +91 -0
- package/adapters/graphql.mjs +28 -0
- package/adapters/index.mjs +56 -0
- package/adapters/node.mjs +118 -0
- package/adapters/object-store.mjs +64 -0
- package/adapters/oidc.mjs +56 -0
- package/adapters/openapi.mjs +50 -0
- package/adapters/pnpm.mjs +42 -0
- package/adapters/protobuf.mjs +35 -0
- package/adapters/registry.mjs +27 -0
- package/adapters/resource-client.mjs +61 -0
- package/adapters/turborepo.mjs +135 -0
- package/adapters/typescript.mjs +85 -0
- package/adapters/wrangler.mjs +206 -0
- package/bin/agentdoc.mjs +494 -0
- package/bin/commands.mjs +169 -0
- package/bin/usage.txt +28 -0
- package/core/acceptances.mjs +69 -0
- package/core/audit.mjs +456 -0
- package/core/authority/classes.mjs +39 -0
- package/core/authority/engine.mjs +266 -0
- package/core/authority/facts.mjs +41 -0
- package/core/codes.mjs +126 -0
- package/core/compile.mjs +1018 -0
- package/core/contracts.mjs +121 -0
- package/core/derive.mjs +102 -0
- package/core/descriptors.mjs +489 -0
- package/core/determinism.mjs +95 -0
- package/core/discover.mjs +147 -0
- package/core/facts.mjs +53 -0
- package/core/fsx.mjs +348 -0
- package/core/graph.mjs +260 -0
- package/core/impact.mjs +175 -0
- package/core/indexes.mjs +165 -0
- package/core/jsonschema.mjs +298 -0
- package/core/normative.mjs +171 -0
- package/core/observations.mjs +129 -0
- package/core/provenance.mjs +83 -0
- package/core/query.mjs +384 -0
- package/core/relations.mjs +286 -0
- package/core/scaffold.mjs +322 -0
- package/core/secrets.mjs +92 -0
- package/core/sourcescan.mjs +88 -0
- package/core/toml.mjs +163 -0
- package/core/yaml.mjs +477 -0
- package/docs/ADAPTERS.md +207 -0
- package/docs/AUTHORITY.md +269 -0
- package/docs/CI.md +138 -0
- package/docs/COMPARISON-KODA.md +164 -0
- package/docs/EVALS.md +166 -0
- package/docs/MIGRATION.md +261 -0
- package/docs/PROVENANCE.md +133 -0
- package/docs/QUERY.md +180 -0
- package/docs/SPEC.md +339 -0
- package/docs/VERIFICATION.md +28 -0
- package/docs/assets/readme/agentdoc-hero.webp +0 -0
- package/docs/assets/readme/agentdoc-in-action.webp +0 -0
- package/docs/assets/readme/how-agentdoc-works.webp +0 -0
- package/docs/assets/readme/why-agentdoc.webp +0 -0
- package/docs/ci/generic-ci.sh +28 -0
- package/docs/ci/github-actions.yml +35 -0
- package/evals/harness.mjs +426 -0
- package/evals/scenarios/fixture-a-01-change-api.json +9 -0
- package/evals/scenarios/fixture-a-02-change-db.json +9 -0
- package/evals/scenarios/fixture-a-03-enforce-constraint.json +9 -0
- package/evals/scenarios/fixture-b-01-change-proto.json +9 -0
- package/evals/scenarios/fixture-b-02-change-write-path.json +9 -0
- package/evals/scenarios/fixture-b-03-change-shared-lib.json +9 -0
- package/evals/scenarios/fixture-b-04-journey.json +9 -0
- package/evals/scenarios/fixture-c-01-mixed-runtime-edge.json +19 -0
- package/evals/scenarios/fixture-c-02-stale-doc-contradiction.json +9 -0
- package/evals/scenarios/fixture-c-03-shared-vocabulary.json +9 -0
- package/evals/scenarios/fixture-c-04-contract-change.json +9 -0
- package/evals/scenarios/koda-01-modify-api-consumer.json +26 -0
- package/evals/scenarios/koda-02-change-db-schema.json +20 -0
- package/evals/scenarios/koda-03-change-auth.json +21 -0
- package/evals/scenarios/koda-04-add-event-producer.json +19 -0
- package/evals/scenarios/koda-05-modify-worker-binding.json +25 -0
- package/evals/scenarios/koda-06-change-cross-component-endpoint.json +22 -0
- package/evals/scenarios/koda-07-change-shared-package.json +16 -0
- package/evals/scenarios/koda-08-change-runtime-cron.json +23 -0
- package/evals/scenarios/koda-09-debug-production-mismatch.json +26 -0
- package/evals/thresholds.json +11 -0
- package/package.json +46 -4
- package/schemas/api.schema.json +84 -0
- package/schemas/common.schema.json +147 -0
- package/schemas/component.schema.json +44 -0
- package/schemas/config.schema.json +801 -0
- package/schemas/domain.schema.json +14 -0
- package/schemas/graph.schema.json +1084 -0
- package/schemas/journeys.schema.json +47 -0
- package/schemas/observation.schema.json +143 -0
- package/schemas/resource.schema.json +28 -0
- package/schemas/system.schema.json +26 -0
- package/skills/agent-doc-system/SKILL.md +117 -0
- package/skills/agent-doc-system/references/authority-and-conflicts.md +70 -0
- package/skills/agent-doc-system/references/cli.md +97 -0
- package/skills/agent-doc-system/references/create-migrate.md +113 -0
- package/skills/agent-doc-system/references/maintenance.md +52 -0
- package/templates/ADR.md +26 -0
- package/templates/ARCHITECTURE.md +19 -0
- package/templates/CONSTRAINTS.md +17 -0
- package/templates/PRODUCT.md +17 -0
- package/templates/api.yaml +10 -0
- package/templates/ci.yml +31 -0
- package/templates/component.yaml +14 -0
- package/templates/domain.yaml +6 -0
- package/templates/journey.yaml +7 -0
- package/templates/observation.yaml +19 -0
- package/templates/resource.yaml +8 -0
- package/templates/system.yaml +10 -0
- package/templates/warning-acceptance.yaml +14 -0
package/core/yaml.mjs
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// Strict YAML 1.2 *subset* parser with zero dependencies.
|
|
2
|
+
//
|
|
3
|
+
// Supported: multi-document streams, block mappings, block sequences, plain /
|
|
4
|
+
// single-quoted / double-quoted scalars, flow sequences and mappings of
|
|
5
|
+
// scalars, literal (|) and folded (>) block scalars, comments, and the YAML
|
|
6
|
+
// 1.2 core scalar resolution (null/bool/int/float/string).
|
|
7
|
+
//
|
|
8
|
+
// Deliberately unsupported and rejected with a dedicated code: anchors,
|
|
9
|
+
// aliases, custom tags, merge keys, environment interpolation, complex keys,
|
|
10
|
+
// and any nesting the compiler would have to guess at. Authored sources are
|
|
11
|
+
// machine-generated templates plus human review; ambiguity is a defect, not a
|
|
12
|
+
// feature. See SPEC.md "Source dialect".
|
|
13
|
+
import { AgentDocError, CODES } from "./codes.mjs";
|
|
14
|
+
|
|
15
|
+
const BANNED = [
|
|
16
|
+
{ re: /(?:^|[\s\[])&[A-Za-z0-9_-]+/, code: CODES.YAML_ANCHOR, what: "anchors" },
|
|
17
|
+
{ re: /(?:^|[\s\[])\*[A-Za-z0-9_-]+/, code: CODES.YAML_ALIAS, what: "aliases" },
|
|
18
|
+
{ re: /(?:^|[\s[])!(?:<[^>]*>|![^\s]*|[A-Za-z][A-Za-z0-9_:.\/-]*)/, code: CODES.YAML_TAG, what: "custom tags" },
|
|
19
|
+
{ re: /^\s*<<\s*:/, code: CODES.YAML_MERGE_KEY, what: "merge keys" },
|
|
20
|
+
{ re: /\$\{[A-Za-z_][A-Za-z0-9_]*\}/, code: CODES.YAML_ENV, what: "environment interpolation" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function scanBanned(text, path) {
|
|
24
|
+
const lines = text.split("\n");
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
const line = lines[i];
|
|
27
|
+
if (/^\s*(#|---|\.\.\.)/.test(line) && !/^(---|...)/.test(line.trim())) continue;
|
|
28
|
+
for (const b of BANNED) {
|
|
29
|
+
if (b.re.test(line)) {
|
|
30
|
+
throw new AgentDocError(b.code, "YAML " + b.what + " are forbidden in agentdoc sources", {
|
|
31
|
+
path,
|
|
32
|
+
line: i + 1,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeLine(raw, lineNo) {
|
|
40
|
+
const l = { raw, lineNo, indent: 0, content: "", blank: false, _c: null };
|
|
41
|
+
l.indent = /^[ \t]*/.exec(raw)[0].length;
|
|
42
|
+
if (/^\t/.test(raw)) {
|
|
43
|
+
throw new AgentDocError(CODES.YAML_PARSE, "tab character used for indentation", { line: lineNo });
|
|
44
|
+
}
|
|
45
|
+
const t = raw.trim();
|
|
46
|
+
l.blank = t === "" || t.startsWith("#");
|
|
47
|
+
return l;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stripComment(raw) {
|
|
51
|
+
let q = null;
|
|
52
|
+
for (let i = 0; i < raw.length; i++) {
|
|
53
|
+
const c = raw[i];
|
|
54
|
+
if (q) {
|
|
55
|
+
if (c === "\\" && q === '"') { i++; continue; }
|
|
56
|
+
if (c === q) q = null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (c === '"' || c === "'") { q = c; continue; }
|
|
60
|
+
if (c === "#" && (i === 0 || /\s/.test(raw[i - 1]))) return raw.slice(0, i);
|
|
61
|
+
}
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function content(l) {
|
|
66
|
+
if (l._c === null) l._c = stripComment(l.raw).replace(/\s+$/, "");
|
|
67
|
+
return l._c;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isSeqEntry = (s) => s === "-" || /^-(\s)/.test(s);
|
|
71
|
+
|
|
72
|
+
function splitKey(s) {
|
|
73
|
+
// Returns {key, rest} for `key: value` / `key:` / `"key": value`, else null.
|
|
74
|
+
if (s === "") return null;
|
|
75
|
+
if (s[0] === '"' || s[0] === "'") {
|
|
76
|
+
const q = s[0];
|
|
77
|
+
let i = 1;
|
|
78
|
+
while (i < s.length) {
|
|
79
|
+
if (s[i] === "\\" && q === '"') { i += 2; continue; }
|
|
80
|
+
if (s[i] === q) {
|
|
81
|
+
if (q === "'" && s[i + 1] === "'") { i += 2; continue; }
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
if (i >= s.length) return null;
|
|
87
|
+
const key = unquote(s.slice(0, i + 1));
|
|
88
|
+
const after = s.slice(i + 1);
|
|
89
|
+
if (!after.startsWith(":")) return null;
|
|
90
|
+
return { key, rest: after.slice(1).replace(/^[ \t]+/, "").replace(/\s+$/, "") };
|
|
91
|
+
}
|
|
92
|
+
let depth = 0;
|
|
93
|
+
for (let i = 0; i < s.length; i++) {
|
|
94
|
+
const c = s[i];
|
|
95
|
+
if (c === "[" || c === "{") depth++;
|
|
96
|
+
else if (c === "]" || c === "}") depth--;
|
|
97
|
+
else if (c === ":" && depth === 0) {
|
|
98
|
+
const nxt = s[i + 1];
|
|
99
|
+
if (nxt === undefined || nxt === " " || nxt === "\t") {
|
|
100
|
+
return { key: s.slice(0, i).replace(/\s+$/, ""), rest: s.slice(i + 1).replace(/^[ \t]+/, "").replace(/\s+$/, "") };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function unquote(tok) {
|
|
108
|
+
const q = tok[0];
|
|
109
|
+
const body = tok.slice(1, -1);
|
|
110
|
+
if (q === "'") return body.replace(/''/g, "'");
|
|
111
|
+
let out = "";
|
|
112
|
+
for (let i = 0; i < body.length; i++) {
|
|
113
|
+
const c = body[i];
|
|
114
|
+
if (c !== "\\") { out += c; continue; }
|
|
115
|
+
const n = body[++i];
|
|
116
|
+
if (n === "n") out += "\n";
|
|
117
|
+
else if (n === "t") out += "\t";
|
|
118
|
+
else if (n === "r") out += "\r";
|
|
119
|
+
else if (n === "0") out += "\0";
|
|
120
|
+
else if (n === '"') out += '"';
|
|
121
|
+
else if (n === "\\") out += "\\";
|
|
122
|
+
else if (n === "/") out += "/";
|
|
123
|
+
else if (n === "u") { out += String.fromCharCode(parseInt(body.slice(i + 1, i + 5), 16)); i += 4; }
|
|
124
|
+
else throw new AgentDocError(CODES.YAML_UNSUPPORTED, "unsupported escape sequence \\" + n);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const INT_RE = /^[-+]?[0-9]+$/;
|
|
130
|
+
const OCT_RE = /^0o[0-7]+$/;
|
|
131
|
+
const HEX_RE = /^0x[0-9a-fA-F]+$/;
|
|
132
|
+
const FLOAT_RE = /^[-+]?(?:[0-9]+\.[0-9]*|\.[0-9]+|[0-9]+)(?:[eE][-+]?[0-9]+)?$/;
|
|
133
|
+
|
|
134
|
+
function resolvePlain(s) {
|
|
135
|
+
if (s === "" || s === "null" || s === "~" || s === "Null" || s === "NULL") return null;
|
|
136
|
+
if (s === "true" || s === "True" || s === "TRUE") return true;
|
|
137
|
+
if (s === "false" || s === "False" || s === "FALSE") return false;
|
|
138
|
+
if (INT_RE.test(s)) {
|
|
139
|
+
const n = Number(s);
|
|
140
|
+
if (!Number.isSafeInteger(n)) throw new AgentDocError(CODES.YAML_UNSUPPORTED, "integer out of safe range: " + s);
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
if (OCT_RE.test(s)) return parseInt(s.slice(2), 8);
|
|
144
|
+
if (HEX_RE.test(s)) return parseInt(s.slice(2), 16);
|
|
145
|
+
if (FLOAT_RE.test(s) && /[.eE]/.test(s)) return Number(s);
|
|
146
|
+
if (s === ".inf" || s === ".Inf") return Infinity;
|
|
147
|
+
if (s === "-.inf" || s === "-.Inf") return -Infinity;
|
|
148
|
+
if (s === ".nan" || s === ".NaN") return NaN;
|
|
149
|
+
return s;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function scalar(tok, lineNo) {
|
|
153
|
+
const t = tok.trim();
|
|
154
|
+
if (t === "") return null;
|
|
155
|
+
if (t[0] === '"' || t[0] === "'") {
|
|
156
|
+
if (t.length < 2 || t[t.length - 1] !== t[0]) {
|
|
157
|
+
throw new AgentDocError(CODES.YAML_PARSE, "unterminated quoted scalar", { line: lineNo });
|
|
158
|
+
}
|
|
159
|
+
return unquote(t);
|
|
160
|
+
}
|
|
161
|
+
if (t[0] === "[" || t[0] === "{") return flow(t, lineNo);
|
|
162
|
+
if (t[0] === "&" || t[0] === "*" || t[0] === "!") {
|
|
163
|
+
throw new AgentDocError(CODES.YAML_UNSUPPORTED, "anchors, aliases and tags are not supported", { line: lineNo });
|
|
164
|
+
}
|
|
165
|
+
return resolvePlain(t);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Flow collections of scalars (and nested flow collections).
|
|
169
|
+
function flow(text, lineNo) {
|
|
170
|
+
let i = 0;
|
|
171
|
+
const err = (m) => { throw new AgentDocError(CODES.YAML_PARSE, "flow collection: " + m, { line: lineNo }); };
|
|
172
|
+
const ws = () => { while (i < text.length && /\s/.test(text[i])) i++; };
|
|
173
|
+
let valueStart = 0;
|
|
174
|
+
function value() {
|
|
175
|
+
ws();
|
|
176
|
+
const c = text[i];
|
|
177
|
+
if (c === "[") return seqFlow();
|
|
178
|
+
if (c === "{") return mapFlow();
|
|
179
|
+
valueStart = i;
|
|
180
|
+
return scalarToken();
|
|
181
|
+
}
|
|
182
|
+
function scalarToken() {
|
|
183
|
+
ws();
|
|
184
|
+
if (text[i] === '"' || text[i] === "'") {
|
|
185
|
+
valueStart = i;
|
|
186
|
+
const q = text[i++];
|
|
187
|
+
let out = "";
|
|
188
|
+
while (i < text.length) {
|
|
189
|
+
if (q === '"' && text[i] === "\\") { out += text[i] + text[i + 1]; i += 2; continue; }
|
|
190
|
+
if (text[i] === q) {
|
|
191
|
+
if (q === "'" && text[i + 1] === "'") { out += "'"; i += 2; continue; }
|
|
192
|
+
i++;
|
|
193
|
+
return unquote(q + out + q);
|
|
194
|
+
}
|
|
195
|
+
out += text[i++];
|
|
196
|
+
}
|
|
197
|
+
err("unterminated quoted scalar");
|
|
198
|
+
}
|
|
199
|
+
// Scan a plain scalar in flow context. It ends at a bracket, at a colon, or
|
|
200
|
+
// at a comma that is a separator; see isSeqSeparator.
|
|
201
|
+
while (i < text.length) {
|
|
202
|
+
const c = text[i];
|
|
203
|
+
if (c === "]" || c === "}") break;
|
|
204
|
+
if (c === ":") break;
|
|
205
|
+
if (c === ",") {
|
|
206
|
+
const save = i;
|
|
207
|
+
if (isSeqSeparator()) { i = save; break; }
|
|
208
|
+
i++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
const t = text.slice(valueStart, i).replace(/\s+$/, "");
|
|
214
|
+
return resolvePlain(t);
|
|
215
|
+
}
|
|
216
|
+
function seqFlow() {
|
|
217
|
+
i++; // [
|
|
218
|
+
const out = [];
|
|
219
|
+
ws();
|
|
220
|
+
if (text[i] === "]") { i++; return out; }
|
|
221
|
+
for (;;) {
|
|
222
|
+
out.push(value());
|
|
223
|
+
ws();
|
|
224
|
+
if (isSeqSeparator()) { i++; continue; }
|
|
225
|
+
if (text[i] === "]") { i++; return out; }
|
|
226
|
+
err("expected , or ]");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function mapFlow() {
|
|
230
|
+
i++; // {
|
|
231
|
+
const out = {};
|
|
232
|
+
ws();
|
|
233
|
+
if (text[i] === "}") { i++; return out; }
|
|
234
|
+
for (;;) {
|
|
235
|
+
ws();
|
|
236
|
+
valueStart = i;
|
|
237
|
+
const k = scalarKey();
|
|
238
|
+
ws();
|
|
239
|
+
if (text[i] !== ":") err("expected : in flow mapping");
|
|
240
|
+
i++;
|
|
241
|
+
const v = value();
|
|
242
|
+
if (Object.prototype.hasOwnProperty.call(out, k)) {
|
|
243
|
+
throw new AgentDocError(CODES.YAML_DUP_KEY, "duplicate mapping key " + JSON.stringify(k), { line: lineNo });
|
|
244
|
+
}
|
|
245
|
+
out[k] = v;
|
|
246
|
+
ws();
|
|
247
|
+
if (isSeqSeparator()) { i++; continue; }
|
|
248
|
+
if (text[i] === "}") { i++; return out; }
|
|
249
|
+
err("expected , or }");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// A flow-mapping key is a plain or quoted scalar ending at a colon. Keys are
|
|
253
|
+
// never allowed to swallow a comma, so this is the strict form.
|
|
254
|
+
function scalarKey() {
|
|
255
|
+
if (text[i] === '"' || text[i] === "'") {
|
|
256
|
+
const q = text[i++];
|
|
257
|
+
let out = "";
|
|
258
|
+
while (i < text.length) {
|
|
259
|
+
if (text[i] === "\\" && q === '"') { out += text[i] + text[i + 1]; i += 2; continue; }
|
|
260
|
+
if (text[i] === q) {
|
|
261
|
+
if (q === "'" && text[i + 1] === "'") { out += "'"; i += 2; continue; }
|
|
262
|
+
i++;
|
|
263
|
+
return unquote(q + out + q);
|
|
264
|
+
}
|
|
265
|
+
out += text[i++];
|
|
266
|
+
}
|
|
267
|
+
err("unterminated quoted key");
|
|
268
|
+
}
|
|
269
|
+
const start = i;
|
|
270
|
+
while (i < text.length && !":,]}".includes(text[i])) i++;
|
|
271
|
+
const t = text.slice(start, i).replace(/\s+$/, "");
|
|
272
|
+
if (t.includes(": ")) err("quoted keys are required in flow mappings");
|
|
273
|
+
return resolvePlain(t);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// A comma inside a flow collection is a separator only when what follows
|
|
277
|
+
// looks like the next element. Real OpenAPI documents contain flow mappings
|
|
278
|
+
// whose plain-scalar values contain commas — `{ description: Body is
|
|
279
|
+
// unreadable, invalid, or wrong. }` — and the YAML ecosystem parses them, so
|
|
280
|
+
// refusing them would reject valid-in-practice contracts. The rule below is
|
|
281
|
+
// deterministic and matches that behaviour: a comma separates when the
|
|
282
|
+
// remainder is a closing bracket, a new key (`ident:`), or the previous token
|
|
283
|
+
// carried no internal whitespace.
|
|
284
|
+
function isSeqSeparator() {
|
|
285
|
+
if (text[i] !== ",") return false;
|
|
286
|
+
const rest = text.slice(i + 1);
|
|
287
|
+
if (/^\s*[}\]]/.test(rest)) return true;
|
|
288
|
+
if (/^\s*[\[{]/.test(rest)) return true;
|
|
289
|
+
if (/^\s*[^,:{}\[\]"']+\s*:/.test(rest)) return true;
|
|
290
|
+
const prev = text.slice(valueStart, i);
|
|
291
|
+
return prev.length > 0 && !/\s/.test(prev);
|
|
292
|
+
}
|
|
293
|
+
const v = value();
|
|
294
|
+
ws();
|
|
295
|
+
if (i !== text.length) err("trailing content after flow collection");
|
|
296
|
+
return v;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
class Reader {
|
|
300
|
+
constructor(lines, path) {
|
|
301
|
+
this.ls = lines;
|
|
302
|
+
this.path = path;
|
|
303
|
+
}
|
|
304
|
+
skip() {
|
|
305
|
+
while (this.i < this.ls.length && this.ls[this.i].blank) this.i++;
|
|
306
|
+
}
|
|
307
|
+
peek() {
|
|
308
|
+
this.skip();
|
|
309
|
+
return this.i < this.ls.length ? this.ls[this.i] : null;
|
|
310
|
+
}
|
|
311
|
+
node(indent) {
|
|
312
|
+
const l = this.peek();
|
|
313
|
+
if (!l || l.indent < indent) return null;
|
|
314
|
+
if (isSeqEntry(content(l).slice(l.indent))) return this.seq(l.indent);
|
|
315
|
+
return this.map(l.indent);
|
|
316
|
+
}
|
|
317
|
+
map(indent) {
|
|
318
|
+
const out = {};
|
|
319
|
+
for (;;) {
|
|
320
|
+
const l = this.peek();
|
|
321
|
+
if (!l || l.indent < indent) break;
|
|
322
|
+
const s = content(l).slice(l.indent);
|
|
323
|
+
if (isSeqEntry(s)) {
|
|
324
|
+
throw new AgentDocError(CODES.YAML_PARSE, "sequence entry where a mapping key was expected", {
|
|
325
|
+
path: this.path, line: l.lineNo,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (l.indent > indent) {
|
|
329
|
+
throw new AgentDocError(CODES.YAML_PARSE, "unexpected indentation", { path: this.path, line: l.lineNo });
|
|
330
|
+
}
|
|
331
|
+
const kv = splitKey(s);
|
|
332
|
+
if (!kv) {
|
|
333
|
+
throw new AgentDocError(CODES.YAML_PARSE, "expected 'key: value', got " + JSON.stringify(s.slice(0, 40)), {
|
|
334
|
+
path: this.path, line: l.lineNo,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (Object.prototype.hasOwnProperty.call(out, kv.key)) {
|
|
338
|
+
throw new AgentDocError(CODES.YAML_DUP_KEY, "duplicate mapping key " + JSON.stringify(kv.key), {
|
|
339
|
+
path: this.path, line: l.lineNo,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
this.i++;
|
|
343
|
+
out[kv.key] = this.value(l, kv.rest, indent);
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
seq(indent) {
|
|
348
|
+
const out = [];
|
|
349
|
+
for (;;) {
|
|
350
|
+
const l = this.peek();
|
|
351
|
+
if (!l || l.indent < indent) break;
|
|
352
|
+
const s = content(l).slice(l.indent);
|
|
353
|
+
if (!isSeqEntry(s)) {
|
|
354
|
+
if (l.indent > indent) {
|
|
355
|
+
throw new AgentDocError(CODES.YAML_PARSE, "unexpected indentation in sequence", { path: this.path, line: l.lineNo });
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
const rest = s.slice(1).replace(/^[ \t]+/, "");
|
|
360
|
+
if (rest === "") {
|
|
361
|
+
this.i++;
|
|
362
|
+
out.push(this.node(indent + 1));
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const dashOffset = l.indent + (s.length - rest.length);
|
|
366
|
+
if (splitKey(rest)) {
|
|
367
|
+
// Inline mapping opened by the dash: re-anchor it at the text column so
|
|
368
|
+
// continuation lines line up naturally.
|
|
369
|
+
this.ls[this.i] = { raw: " ".repeat(dashOffset) + rest, lineNo: l.lineNo, indent: dashOffset, blank: false, _c: null };
|
|
370
|
+
out.push(this.map(dashOffset));
|
|
371
|
+
} else {
|
|
372
|
+
this.i++;
|
|
373
|
+
out.push(this.scalarValue(l, rest, indent));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
value(l, rest, indent) {
|
|
379
|
+
if (rest === "") return this.node(indent + 1);
|
|
380
|
+
if (rest[0] === "|" || rest[0] === ">") return this.blockScalar(l, rest, indent);
|
|
381
|
+
return scalar(rest, l.lineNo);
|
|
382
|
+
}
|
|
383
|
+
scalarValue(l, rest, indent) {
|
|
384
|
+
if (rest[0] === "|" || rest[0] === ">") return this.blockScalar(l, rest, indent);
|
|
385
|
+
return scalar(rest, l.lineNo);
|
|
386
|
+
}
|
|
387
|
+
blockScalar(l, head, indent) {
|
|
388
|
+
const m = /^([|>])([-+]?)([0-9]*)([-+]?)\s*$/.exec(head);
|
|
389
|
+
if (!m) {
|
|
390
|
+
throw new AgentDocError(CODES.YAML_UNSUPPORTED, "unsupported block scalar header " + JSON.stringify(head), {
|
|
391
|
+
path: this.path, line: l.lineNo,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const folded = m[1] === ">";
|
|
395
|
+
const chomp = m[2] || m[4] || "";
|
|
396
|
+
const explicit = m[3] ? Number(m[3]) : 0;
|
|
397
|
+
const body = [];
|
|
398
|
+
let blockIndent = explicit ? indent + explicit : -1;
|
|
399
|
+
while (this.i < this.ls.length) {
|
|
400
|
+
const n = this.ls[this.i];
|
|
401
|
+
const t = n.raw.trim();
|
|
402
|
+
if (t === "" && n.indent === 0) { body.push(""); this.i++; continue; }
|
|
403
|
+
if (n.indent <= indent) break;
|
|
404
|
+
if (blockIndent < 0) blockIndent = n.indent;
|
|
405
|
+
body.push(n.raw.slice(blockIndent));
|
|
406
|
+
this.i++;
|
|
407
|
+
}
|
|
408
|
+
while (body.length && body[body.length - 1] === "") body.pop();
|
|
409
|
+
let text;
|
|
410
|
+
if (folded) {
|
|
411
|
+
const parts = [];
|
|
412
|
+
let cur = "";
|
|
413
|
+
for (const b of body) {
|
|
414
|
+
if (b === "") { parts.push(cur); cur = ""; continue; }
|
|
415
|
+
cur = cur === "" ? b : cur + " " + b;
|
|
416
|
+
}
|
|
417
|
+
parts.push(cur);
|
|
418
|
+
text = parts.join("\n");
|
|
419
|
+
} else {
|
|
420
|
+
text = body.join("\n");
|
|
421
|
+
}
|
|
422
|
+
if (chomp !== "-") text += "\n";
|
|
423
|
+
return text;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Parse a multi-document YAML stream. Returns [{doc, line}] with 1-based line
|
|
428
|
+
// numbers of each document root. Catalog sources must be mappings; a reader of
|
|
429
|
+
// third-party files whose root is a list (an Ansible playbook or task file)
|
|
430
|
+
// opts in with `allowSequenceRoot`.
|
|
431
|
+
export function parseYamlDocuments(src, path, { allowSequenceRoot = false } = {}) {
|
|
432
|
+
if (typeof src !== "string") throw new AgentDocError(CODES.UTF8, "expected text for " + path, { path });
|
|
433
|
+
if (src.charCodeAt(0) === 0xfeff) src = src.slice(1);
|
|
434
|
+
scanBanned(src, path);
|
|
435
|
+
const all = src.split("\n").map((raw, i) => makeLine(raw.replace(/\r$/, ""), i + 1));
|
|
436
|
+
const out = [];
|
|
437
|
+
let start = 0;
|
|
438
|
+
const bounds = [];
|
|
439
|
+
for (let i = 0; i < all.length; i++) {
|
|
440
|
+
const t = all[i].raw;
|
|
441
|
+
if (/^---(\s|$)/.test(t)) { bounds.push({ kind: "start", i }); }
|
|
442
|
+
else if (/^\.\.\.(\s|$)/.test(t)) { bounds.push({ kind: "end", i }); }
|
|
443
|
+
}
|
|
444
|
+
const segs = [];
|
|
445
|
+
let from = 0;
|
|
446
|
+
for (const b of bounds) {
|
|
447
|
+
if (b.kind === "start") { if (b.i > from) segs.push([from, b.i]); from = b.i + 1; }
|
|
448
|
+
else { segs.push([from, b.i]); from = b.i + 1; }
|
|
449
|
+
}
|
|
450
|
+
segs.push([from, all.length]);
|
|
451
|
+
for (const [a, b] of segs) {
|
|
452
|
+
const lines = all.slice(a, b);
|
|
453
|
+
const r = new Reader(lines, path);
|
|
454
|
+
r.i = 0;
|
|
455
|
+
const first = r.peek();
|
|
456
|
+
if (!first) continue;
|
|
457
|
+
const rootLine = first.lineNo;
|
|
458
|
+
const value = r.node(first.indent);
|
|
459
|
+
if (r.peek()) {
|
|
460
|
+
throw new AgentDocError(CODES.YAML_PARSE, "unparsed trailing content", { path, line: r.peek().lineNo });
|
|
461
|
+
}
|
|
462
|
+
if (value === null) continue;
|
|
463
|
+
if (typeof value !== "object" || (Array.isArray(value) && !allowSequenceRoot)) {
|
|
464
|
+
throw new AgentDocError(CODES.YAML_NON_MAP_ROOT, "document root must be a mapping", { path, line: rootLine });
|
|
465
|
+
}
|
|
466
|
+
out.push({ doc: value, line: rootLine });
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function parseYamlOne(src, path) {
|
|
472
|
+
const docs = parseYamlDocuments(src, path);
|
|
473
|
+
if (docs.length !== 1) {
|
|
474
|
+
throw new AgentDocError(CODES.YAML_PARSE, "expected exactly one YAML document in " + path, { path });
|
|
475
|
+
}
|
|
476
|
+
return docs[0].doc;
|
|
477
|
+
}
|
package/docs/ADAPTERS.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# ADAPTERS
|
|
2
|
+
|
|
3
|
+
An adapter knows one technology's vocabulary. It does not know your repository.
|
|
4
|
+
|
|
5
|
+
## The contract
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
class MyAdapter extends Adapter {
|
|
9
|
+
static adapterName = "my-technology";
|
|
10
|
+
|
|
11
|
+
constructor() { super({ name: "my-technology", version: "1.0.0", kind: "unit-marker" }); }
|
|
12
|
+
|
|
13
|
+
// Unit-marker adapters: does this directory host a unit of my kind?
|
|
14
|
+
detect(ctx, root) { return { path: root + "/manifest.toml", shape: "deployable" }; }
|
|
15
|
+
|
|
16
|
+
// Contribute workspace member roots the descriptor globs cannot know about.
|
|
17
|
+
roots(ctx) { return [{ root: "modules/a", importable: true }]; }
|
|
18
|
+
|
|
19
|
+
// Per-unit extraction. Return plain data; the pipeline merges it.
|
|
20
|
+
extract(ctx, unit) { return { languages: new Set(["mylang"]), bindings: [...] }; }
|
|
21
|
+
|
|
22
|
+
// Contract indexing. Runs once, after every unit is known.
|
|
23
|
+
contracts(ctx) { ctx.addContract({ apiRef, ref, operations: [...] }); }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Available on `ctx`:
|
|
29
|
+
|
|
30
|
+
| member | purpose |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `repo` | tracked repository access (`readText`, `exists`, `isDir`, `walk`, `git`, `diffPaths`) |
|
|
33
|
+
| `cfg` | the parsed configuration |
|
|
34
|
+
| `refs` | `refFor`, `kindOfRef`, `isValidRef` for this repository's namespace |
|
|
35
|
+
| `prov.add(class, path, extractor, subjects)` | create a provenance record |
|
|
36
|
+
| `diagnostics.push({ severity, code, message, ... })` | a warning or an error |
|
|
37
|
+
| `addFact(subject, key, value, { evidenceClass, confidence, provRecs, semantics, observed, review })` | record a claim |
|
|
38
|
+
| `addEdge(type, sourceRef, targetRef, attributes, provRecs, evidenceClass)` | propose a relation |
|
|
39
|
+
| `addEvent(pattern, role, componentRef, provRec, contract)` | an event subject |
|
|
40
|
+
| `addExternal / addCapability` | indexes |
|
|
41
|
+
| `addVerification({ id, componentRefs, tier, command, configPaths, provRecs })` | a component-scoped check |
|
|
42
|
+
| `addGate({ id, tier, command, configPaths, provRecs })` | a repository-scoped check |
|
|
43
|
+
| `transitiveDependencies(ref)` | manifest-declared dependencies, order-independent |
|
|
44
|
+
| `productionDependencies(ref)` | production dependency names, for client-of-resource rules |
|
|
45
|
+
| `deferSecondPass(fn)` | run after every unit is known |
|
|
46
|
+
| `requirements` | declared normative requirements (`subject`, `key`, `value`, `source`, `satisfiedBy`, `expects`) an adapter may derive the implementation side of |
|
|
47
|
+
| `unitByPkgName`, `unitByGoModule` | cross-unit resolution built before extraction |
|
|
48
|
+
| `errors.push({ code, message })` | a hard error, collected rather than thrown |
|
|
49
|
+
|
|
50
|
+
## The three rules
|
|
51
|
+
|
|
52
|
+
### 1. An adapter may not know the repository
|
|
53
|
+
|
|
54
|
+
No folder names, no project names, no domain vocabulary, no provider names, no
|
|
55
|
+
event prefixes, no organisation. A `koda.` subject prefix is configuration
|
|
56
|
+
(`discovery.eventPatternPrefixes`), not code. A `dist/` directory is a generated
|
|
57
|
+
directory, not a component.
|
|
58
|
+
|
|
59
|
+
If you catch yourself writing a project name in an adapter, the fact belongs in
|
|
60
|
+
`agentdoc.config.yaml`.
|
|
61
|
+
|
|
62
|
+
### 2. An adapter may not assert
|
|
63
|
+
|
|
64
|
+
An adapter extracts. It does not decide. If your extractor can produce two
|
|
65
|
+
answers, it must produce **neither** and a diagnostic:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
if (matches.length > 1) {
|
|
69
|
+
ctx.diagnostics.push({ severity: "warning", code: "AGENTDOC_BINDING_AMBIGUOUS", ... });
|
|
70
|
+
return; // not: pick matches[0]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A candidate that is the only candidate is still a candidate, and candidates are
|
|
75
|
+
never elected. Migration authority is not database ownership. A folder that
|
|
76
|
+
looks like a service is not a service.
|
|
77
|
+
|
|
78
|
+
### 3. Every fact needs evidence
|
|
79
|
+
|
|
80
|
+
Anything an adapter adds carries provenance records it created itself. If you
|
|
81
|
+
cannot name the file a fact came from, you cannot add the fact.
|
|
82
|
+
|
|
83
|
+
## Shipped adapters
|
|
84
|
+
|
|
85
|
+
| adapter | recognises | contributes |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `generic` | — | health-route literals, a single-port fact |
|
|
88
|
+
| `node` | `package.json` | importability, npm dependency edges, script tiers |
|
|
89
|
+
| `pnpm` | `pnpm-workspace.yaml` | workspace member roots |
|
|
90
|
+
| `turborepo` | `turbo.json` (nearest ancestor) | per-package task commands |
|
|
91
|
+
| `typescript` | `tsconfig.json` | language, project references, file-router routes |
|
|
92
|
+
| `go` | `go.mod`, `go.work` | module roots, module graph, `go test`/`go vet` |
|
|
93
|
+
| `openapi` | `*.openapi.y[a]ml` | operation inventory, deterministic consumers |
|
|
94
|
+
| `protobuf` / `graphql` | contract files | method/type inventory, consumers |
|
|
95
|
+
| `oidc` | discovery-path literals | OIDC client consumers |
|
|
96
|
+
| `github-actions` | repository-root `.github/workflows` (not a unit marker; a nested one is ignored, as GitHub ignores it) | component-scoped checks and repository gates |
|
|
97
|
+
| `dockerfile` | `Dockerfile`, `Containerfile` | deployable evidence |
|
|
98
|
+
| `deploy-config` | `fly.toml`, `Procfile`, … | deployable evidence |
|
|
99
|
+
| `wrangler` | `wrangler.toml/json` | bindings, crons, service bindings, trigger targets |
|
|
100
|
+
| `database` | migrations, ORM config, pool evidence | logical-database ownership, or a candidate |
|
|
101
|
+
| `object-store` | bucket literals | logical-dataset edges, or an ambiguity |
|
|
102
|
+
| `cache` | cache client evidence | cache bindings |
|
|
103
|
+
| `events` | subject literals, declared constants, event-contract files | producers, consumers, reserved subjects |
|
|
104
|
+
| `ansible` | playbooks (YAML lists of plays) | deployable evidence, installed paths and managed services, the DERIVED side of provisioning requirements |
|
|
105
|
+
|
|
106
|
+
`generic` is always on. Everything else is opt-in through `adapters: [...]`, and
|
|
107
|
+
an unknown name is `AGENTDOC_CONFIG`, not a silent no-op.
|
|
108
|
+
|
|
109
|
+
### `ansible`
|
|
110
|
+
|
|
111
|
+
Reads playbooks under a unit root and follows what they reach — plays,
|
|
112
|
+
`roles:`, `include_role`/`import_role`, `include_tasks`/`import_tasks`,
|
|
113
|
+
`import_playbook`, `block`/`rescue`/`always` — inside the repository only.
|
|
114
|
+
Nothing is executed and no Jinja is evaluated.
|
|
115
|
+
|
|
116
|
+
| it derives | from |
|
|
117
|
+
|---|---|
|
|
118
|
+
| an installed path | `copy`/`template`/`get_url`/`git`/`synchronize` `dest`, `unarchive` `dest`/`creates`, `pip` `virtualenv`, `file` with `state: directory\|touch\|link\|hard`, `lineinfile`/`blockinfile` with `create: true`, a command's literal `creates` |
|
|
119
|
+
| a managed service | `systemd`, `systemd_service`, `service` `name` |
|
|
120
|
+
|
|
121
|
+
The inventory lands on the component as `derived.provisioning`. For a normative
|
|
122
|
+
requirement on that component with `satisfiedBy: [ansible]` and an `expects`
|
|
123
|
+
(`installs: <path>` or `service: <name>`), it adds a `DERIVED` fact on the
|
|
124
|
+
requirement key:
|
|
125
|
+
|
|
126
|
+
| value | confidence | when |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `provided` | deterministic | a reachable task without `when` installs the path (or a path below it), or manages the service |
|
|
129
|
+
| `provided` | candidate | the only matching task is conditional (`when`, `rescue`, a conditional role or include) |
|
|
130
|
+
| `absent` | deterministic | no reachable task matches **and** every reachable task is transparent |
|
|
131
|
+
| `absent` | candidate | no task matches, but something opaque was reached |
|
|
132
|
+
|
|
133
|
+
**What it cannot see, and therefore never rules out:** a command
|
|
134
|
+
(`shell`/`command`/`script`/`raw`) without a literal `creates`; a module it does
|
|
135
|
+
not model (`apt`, `dnf`, `package`, collections, …); a templated path or a
|
|
136
|
+
`loop`; a role, include or imported playbook that is not in the repository or is
|
|
137
|
+
templated; a file that does not parse. Any of these makes an absence a
|
|
138
|
+
candidate, which is never elected — the requirement stays
|
|
139
|
+
`AGENTDOC_REQUIREMENT_UNVERIFIED` and the diagnostic names what was opaque.
|
|
140
|
+
Inventory files, `group_vars`/`host_vars` and variables are not read.
|
|
141
|
+
|
|
142
|
+
`asyncapi` is a contract **type** (declared on an API descriptor, validated by
|
|
143
|
+
the contract layer), not an adapter — there is no `asyncapi` entry in
|
|
144
|
+
`adapters: [...]`.
|
|
145
|
+
|
|
146
|
+
## Enabling
|
|
147
|
+
|
|
148
|
+
Adapters only cost what they read. Start with what the repository demonstrably
|
|
149
|
+
uses; add more when the audit says a fact is missing. Enabling every adapter is
|
|
150
|
+
not free and not always right: `events` with no `eventPatternPrefixes` falls back
|
|
151
|
+
to a conservative three-segment subject shape, which is right for subject-based
|
|
152
|
+
brokers and wrong for a repository that uses dotted identifiers for something
|
|
153
|
+
else entirely.
|
|
154
|
+
|
|
155
|
+
## Determinism
|
|
156
|
+
|
|
157
|
+
An adapter is a pure function of the checkout and the configuration.
|
|
158
|
+
|
|
159
|
+
- Sort everything. `Set` iteration order is insertion order, which depends on
|
|
160
|
+
the order files were read.
|
|
161
|
+
- Never read a clock, an environment variable outside the repository, a random
|
|
162
|
+
value, or an absolute path. The determinism guard scans the graph's structure
|
|
163
|
+
for exactly these, and compilation fails if one appears.
|
|
164
|
+
- Do not depend on execution order. `transitiveDependencies` and
|
|
165
|
+
`unitByPkgName` are computed from manifests before extraction; use them rather
|
|
166
|
+
than reading `ctx.edges`.
|
|
167
|
+
- Bump `version` when behaviour changes. It is part of the graph input hash, so
|
|
168
|
+
an upgrade invalidates every graph — which is correct, because the graph
|
|
169
|
+
changes.
|
|
170
|
+
|
|
171
|
+
## Testing an adapter
|
|
172
|
+
|
|
173
|
+
1. Build a fixture with the smallest repository the adapter must recognise, plus
|
|
174
|
+
one ambiguity it must refuse.
|
|
175
|
+
2. Assert the exact diagnostics and error codes, not message text.
|
|
176
|
+
3. Assert that two compiles produce identical bytes.
|
|
177
|
+
4. Assert the provenance: every added record names a real file and a real hash.
|
|
178
|
+
|
|
179
|
+
The adversarial suite (`tests/adversarial.test.mjs`) is the model: inject a
|
|
180
|
+
defect, assert the right code.
|
|
181
|
+
|
|
182
|
+
## What the core still knows
|
|
183
|
+
|
|
184
|
+
The engine's *layout* knowledge is empty: no folder name, no provider, no domain
|
|
185
|
+
vocabulary. It does, however, know the shape of a manifest filename in three
|
|
186
|
+
places, and those are the honest exceptions worth naming:
|
|
187
|
+
|
|
188
|
+
- `core/compile.mjs` builds a manifest pre-pass over `package.json`, `go.mod`
|
|
189
|
+
and `go.work`, so `unitByPkgName`, `unitByGoModule` and
|
|
190
|
+
`transitiveDependencies` are available to every adapter regardless of
|
|
191
|
+
execution order.
|
|
192
|
+
- `core/impact.mjs` classifies a changed file by consulting the compiled graph —
|
|
193
|
+
its contracts, its provenance paths, its extractor identities — rather than a
|
|
194
|
+
hand-written list of technology filenames. A list would silently under-report
|
|
195
|
+
for every repository that is not Node-and-Go.
|
|
196
|
+
- `core/discover.mjs` has a default list of directories that are never units
|
|
197
|
+
(`agentdoc`, `docs`, `scripts`, `.github`), overridable through
|
|
198
|
+
`discovery.notUnitRoots`.
|
|
199
|
+
|
|
200
|
+
Each is a place a future change should push outward, not a licence to add more.
|
|
201
|
+
|
|
202
|
+
## Adding one to a project
|
|
203
|
+
|
|
204
|
+
Adapters live in the compiler, not in the project, precisely so a project cannot
|
|
205
|
+
extend the fact model into something unreviewable. If your technology is not
|
|
206
|
+
covered, the honest options are to describe the boundary with a canonical
|
|
207
|
+
contract and a `reviewedOverride`, or to contribute an adapter upstream.
|