agent-doc-system 1.0.0 → 1.2.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 (68) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +97 -112
  3. package/adapters/ansible.mjs +306 -0
  4. package/adapters/cache.mjs +8 -2
  5. package/adapters/contract-consumers.mjs +1 -1
  6. package/adapters/database.mjs +18 -4
  7. package/adapters/events.mjs +1 -1
  8. package/adapters/github-actions.mjs +244 -85
  9. package/adapters/go.mjs +1 -1
  10. package/adapters/index.mjs +5 -2
  11. package/adapters/object-store.mjs +2 -2
  12. package/adapters/python.mjs +1142 -0
  13. package/adapters/resource-client.mjs +1 -1
  14. package/adapters/rust.mjs +987 -0
  15. package/adapters/typescript.mjs +1 -1
  16. package/bin/agentdoc.mjs +60 -44
  17. package/bin/commands.mjs +169 -0
  18. package/bin/usage.txt +5 -3
  19. package/core/audit.mjs +55 -4
  20. package/core/authority/engine.mjs +27 -4
  21. package/core/codes.mjs +11 -0
  22. package/core/compile.mjs +263 -13
  23. package/core/dependents.mjs +42 -0
  24. package/core/descriptors.mjs +5 -2
  25. package/core/discover.mjs +60 -9
  26. package/core/fsx.mjs +27 -4
  27. package/core/graph.mjs +1 -1
  28. package/core/impact.mjs +55 -44
  29. package/core/normative.mjs +171 -0
  30. package/core/query.mjs +18 -8
  31. package/core/scaffold.mjs +87 -22
  32. package/core/sourcescan.mjs +11 -2
  33. package/core/toml.mjs +463 -140
  34. package/core/yaml.mjs +5 -3
  35. package/docs/ADAPTERS.md +232 -7
  36. package/docs/AUTHORITY.md +10 -1
  37. package/docs/BENCHMARK.md +68 -0
  38. package/docs/CI.md +3 -1
  39. package/docs/EVALS.md +33 -2
  40. package/docs/MIGRATION.md +76 -6
  41. package/docs/QUERY.md +53 -4
  42. package/docs/SPEC.md +22 -0
  43. package/docs/assets/readme/agentdoc-hero.webp +0 -0
  44. package/docs/assets/readme/agentdoc-in-action.webp +0 -0
  45. package/docs/assets/readme/how-agentdoc-works.webp +0 -0
  46. package/docs/assets/readme/why-agentdoc.webp +0 -0
  47. package/docs/ci/github-actions.yml +1 -1
  48. package/evals/harness.mjs +32 -6
  49. package/evals/scenarios/fixture-b-03-change-shared-lib.json +8 -1
  50. package/evals/scenarios/fixture-c-03-shared-vocabulary.json +7 -1
  51. package/evals/scenarios/fixture-e-01-change-shared-lib.json +14 -0
  52. package/evals/scenarios/fixture-e-02-change-manifest.json +9 -0
  53. package/evals/scenarios/fixture-e-03-local-module.json +9 -0
  54. package/evals/scenarios/fixture-e-04-ambiguous-crate.json +14 -0
  55. package/evals/scenarios/fixture-f-01-change-shared-lib.json +9 -0
  56. package/evals/scenarios/fixture-f-02-change-manifest.json +9 -0
  57. package/evals/scenarios/fixture-f-03-shadowed-import.json +9 -0
  58. package/evals/scenarios/fixture-f-04-nested-workspace.json +9 -0
  59. package/evals/scenarios/fixture-f-05-alembic-database.json +9 -0
  60. package/evals/scenarios/fixture-f-06-unittest-job.json +9 -0
  61. package/package.json +2 -1
  62. package/schemas/common.schema.json +40 -0
  63. package/schemas/component.schema.json +5 -1
  64. package/schemas/config.schema.json +117 -1
  65. package/skills/agent-doc-system/SKILL.md +5 -1
  66. package/skills/agent-doc-system/references/cli.md +12 -4
  67. package/skills/agent-doc-system/references/create-migrate.md +28 -6
  68. package/templates/ci.yml +1 -1
package/core/toml.mjs CHANGED
@@ -1,163 +1,486 @@
1
- // Minimal TOML reader for platform deployment manifests.
1
+ // TOML reader for deployment manifests and Python project files.
2
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.
3
+ // Values follow TOML 1.0: tables, arrays of tables, dotted keys, strings,
4
+ // integers (decimal, hex, octal, binary, with `_` separators), floats
5
+ // (including inf and nan), booleans, dates and times, arrays and inline
6
+ // tables. Safe integers are Numbers; larger integers are exact BigInts.
7
+ // Strings are basic, literal, and both multi-line forms. Literal
8
+ // strings are raw — a backslash is a backslash. Dates and times are returned
9
+ // as the exact lexeme; nothing in the compiler derives a fact from them.
10
+ // Only a form the spec rejects is a diagnostic. A mis-parsed manifest is
11
+ // worse than one, and a valid `[tool.*]` table must not fail the compile.
7
12
  import { AgentDocError, CODES } from "./codes.mjs";
8
13
 
9
14
  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
- }
15
+ // Byte input must be decoded without replacing malformed UTF-8. String
16
+ // callers already own decoding; still reject unpaired UTF-16 surrogates.
17
+ let text;
18
+ try {
19
+ text = src instanceof Uint8Array
20
+ ? new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(src)
21
+ : String(src);
22
+ } catch {
23
+ throw new AgentDocError(CODES.YAML_PARSE, "invalid UTF-8 in TOML", { path, line: 1 });
24
+ }
25
+ const p = new Parser(text.replace(/\r\n/g, "\n"), path);
26
+ const invalid = /[\uD800-\uDFFF]/u.exec(p.s);
27
+ if (invalid) {
28
+ p.line = p.s.slice(0, invalid.index).split("\n").length;
29
+ p.die("invalid unicode scalar");
30
+ }
31
+ return p.parseDocument();
32
+ }
33
+
34
+ class Parser {
35
+ constructor(src, path) {
36
+ this.s = src;
37
+ this.n = src.length;
38
+ this.i = 0;
39
+ this.line = 1;
40
+ this.path = path;
41
+ // Metadata stays outside returned values. Only implicit header parents can
42
+ // later be declared with [table]; inline tables can never be extended.
43
+ this.tables = new WeakMap();
44
+ this.tableArrays = new WeakSet();
45
+ }
46
+
47
+ parseDocument() {
48
+ const root = {};
49
+ let cur = root;
50
+ while (!this.eof()) {
51
+ this.skipIgnorable();
52
+ if (this.eof()) break;
53
+ if (this.peek() === "[") {
54
+ const header = this.parseTableHeader();
55
+ cur = this.openTable(root, header.keys, header.isArray);
56
+ continue;
33
57
  }
34
- continue;
58
+ const keys = this.parseKey();
59
+ this.skipSpaces();
60
+ this.expect("=");
61
+ this.skipSpaces();
62
+ const value = this.parseValue();
63
+ this.assign(cur, keys, value);
64
+ this.endLine();
35
65
  }
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 });
66
+ return root;
67
+ }
68
+
69
+ // ── structure ──────────────────────────────────────────────────────────
70
+
71
+ parseTableHeader() {
72
+ this.expect("[");
73
+ let isArray = false;
74
+ if (this.peek() === "[") {
75
+ isArray = true;
76
+ this.i++;
77
+ this.skipSpaces();
39
78
  }
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();
79
+ const keys = this.parseKey();
80
+ this.skipSpaces();
81
+ this.expect("]");
82
+ if (isArray) this.expect("]");
83
+ this.endLine();
84
+ return { keys, isArray };
85
+ }
86
+
87
+ table(kind) {
88
+ const out = {};
89
+ this.tables.set(out, kind);
90
+ return out;
91
+ }
92
+
93
+ has(table, key) { return Object.prototype.hasOwnProperty.call(table, key); }
94
+
95
+ put(table, key, value) {
96
+ // __proto__, constructor and toString are ordinary TOML keys.
97
+ Object.defineProperty(table, key, { value, enumerable: true, writable: true, configurable: true });
98
+ }
99
+
100
+ openTable(root, keys, isArray) {
101
+ let cur = root;
102
+ for (const key of keys.slice(0, -1)) cur = this.descend(cur, key, true);
103
+ const key = keys.at(-1);
104
+ if (isArray) {
105
+ if (!this.has(cur, key)) {
106
+ const array = [];
107
+ this.tableArrays.add(array);
108
+ this.put(cur, key, array);
109
+ }
110
+ if (!this.tableArrays.has(cur[key])) this.die("key '" + key + "' is not an array of tables");
111
+ const obj = this.table("explicit");
112
+ cur[key].push(obj);
113
+ return obj;
46
114
  }
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]];
115
+ if (!this.has(cur, key)) this.put(cur, key, this.table("implicit"));
116
+ if (this.tables.get(cur[key]) !== "implicit") this.die("duplicate or conflicting table '" + key + "'");
117
+ this.tables.set(cur[key], "explicit");
118
+ return cur[key];
119
+ }
120
+
121
+ descend(cur, key, header = false) {
122
+ if (!this.has(cur, key)) this.put(cur, key, this.table(header ? "implicit" : "dotted"));
123
+ let value = cur[key];
124
+ // A header below an array of tables belongs to its most recent element.
125
+ if (header && this.tableArrays.has(value)) value = value.at(-1);
126
+ const kind = this.tables.get(value);
127
+ if (!kind || kind === "inline" || (!header && kind === "explicit")) {
128
+ this.die("key '" + key + "' cannot contain a nested key");
52
129
  }
53
- target[keys[keys.length - 1]] = value(valueText, path, i + 1);
130
+ return value;
54
131
  }
55
- return root;
56
- }
57
132
 
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
- }
133
+ assign(table, keys, value) {
134
+ let cur = table;
135
+ for (const key of keys.slice(0, -1)) cur = this.descend(cur, key);
136
+ const key = keys.at(-1);
137
+ if (this.has(cur, key)) this.die("duplicate key '" + key + "'");
138
+ this.put(cur, key, value);
139
+ }
70
140
 
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);
141
+ parseKey() {
142
+ const keys = [];
143
+ for (;;) {
144
+ this.skipSpaces();
145
+ keys.push(this.parseKeySegment());
146
+ this.skipSpaces();
147
+ if (this.peek() !== ".") break;
148
+ this.i++;
149
+ }
150
+ return keys;
78
151
  }
79
- return s;
80
- }
81
152
 
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;
153
+ parseKeySegment() {
154
+ const c = this.peek();
155
+ if (c === '"') return this.parseBasicString(false);
156
+ if (c === "'") return this.parseLiteralString(false);
157
+ const start = this.i;
158
+ while (/[A-Za-z0-9_-]/.test(this.peek())) this.i++;
159
+ if (this.i === start) this.die("expected a key");
160
+ return this.s.slice(start, this.i);
89
161
  }
90
- return -1;
91
- }
92
162
 
93
- function splitKeyPath(s) {
94
- return s.split(".").map((k) => unquoteKey(k.trim()));
95
- }
163
+ // ── values ─────────────────────────────────────────────────────────────
96
164
 
97
- function unquoteKey(k) {
98
- if ((k.startsWith('"') && k.endsWith('"')) || (k.startsWith("'") && k.endsWith("'"))) return k.slice(1, -1);
99
- return k;
100
- }
165
+ parseValue() {
166
+ const c = this.peek();
167
+ if (c === '"') return this.s.startsWith('"""', this.i) ? this.parseBasicString(true) : this.parseBasicString(false);
168
+ if (c === "'") return this.s.startsWith("'''", this.i) ? this.parseLiteralString(true) : this.parseLiteralString(false);
169
+ if (c === "[") return this.parseArray();
170
+ if (c === "{") return this.parseInlineTable();
171
+ if (c === "t" && this.startsWithWord("true")) { this.i += 4; return true; }
172
+ if (c === "f" && this.startsWithWord("false")) { this.i += 5; return false; }
173
+ if (c === "+" || c === "-" || c === "i" || c === "n" || (c >= "0" && c <= "9")) return this.parseNumber();
174
+ this.die("unsupported TOML value");
175
+ }
101
176
 
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
- }
177
+ startsWithWord(word) {
178
+ if (!this.s.startsWith(word, this.i)) return false;
179
+ const after = this.s[this.i + word.length];
180
+ return after === undefined || /[\s,\]}#]/.test(after);
181
+ }
114
182
 
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
- }
183
+ parseArray() {
184
+ this.expect("[");
185
+ const out = [];
186
+ for (;;) {
187
+ this.skipIgnorable();
188
+ if (this.peek() === "]") { this.i++; return out; }
189
+ out.push(this.parseValue());
190
+ this.skipIgnorable();
191
+ if (this.peek() === ",") { this.i++; continue; }
192
+ if (this.peek() === "]") { this.i++; return out; }
193
+ this.die("expected ',' or ']' in array");
194
+ }
195
+ }
134
196
 
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);
197
+ // Newlines inside an inline table are TOML 1.1. 1.0 rejects them, and so do
198
+ // we: accepting the break would hide a file this reader does not fully model.
199
+ parseInlineTable() {
200
+ this.expect("{");
201
+ const out = this.table("inline");
202
+ this.skipSpaces();
203
+ if (this.peek() === "}") { this.i++; return out; }
204
+ for (;;) {
205
+ this.skipSpaces();
206
+ if (this.peek() === "\n") this.die("newline in an inline table is not supported");
207
+ const keys = this.parseKey();
208
+ this.skipSpaces();
209
+ this.expect("=");
210
+ this.skipSpaces();
211
+ if (this.peek() === "\n") this.die("newline in an inline table is not supported");
212
+ const value = this.parseValue();
213
+ this.assign(out, keys, value);
214
+ this.skipSpaces();
215
+ if (this.peek() === ",") { this.i++; continue; }
216
+ if (this.peek() === "}") { this.i++; return out; }
217
+ if (this.peek() === "\n") this.die("newline in an inline table is not supported");
218
+ this.die("expected ',' or '}' in inline table");
219
+ }
220
+ }
221
+
222
+ parseBasicString(multi) {
223
+ const delim = multi ? '"""' : '"';
224
+ this.i += delim.length;
225
+ if (multi && this.peek() === "\n") { this.i++; this.line++; }
226
+ let out = "";
227
+ while (!this.eof()) {
228
+ if (!multi && this.peek() === "\n") this.die("unterminated basic string");
229
+ if (this.s.startsWith(delim, this.i)) {
230
+ if (!multi) { this.i++; return out; }
231
+ // One or two quotes may precede the closing three, but never three.
232
+ let q = 3;
233
+ while (this.s[this.i + q] === '"') q++;
234
+ if (q > 5) this.die("too many quotes at end of basic string");
235
+ this.i += q;
236
+ return out + '"'.repeat(q - 3);
237
+ }
238
+ const c = this.peek();
239
+ if (c === "\\") {
240
+ if (multi && this.lineEndingBackslash()) continue;
241
+ out += this.basicEscape();
242
+ continue;
243
+ }
244
+ this.rejectControl(c, multi);
245
+ if (c === "\n") this.line++;
246
+ out += c;
247
+ this.i++;
248
+ }
249
+ this.die("unterminated basic string");
250
+ }
251
+
252
+ // A `\` that is the last non-whitespace character on the line is not an
253
+ // escape. It deletes itself, the line break, and the following whitespace.
254
+ lineEndingBackslash() {
255
+ let j = this.i + 1;
256
+ while (j < this.n && (this.s[j] === " " || this.s[j] === "\t")) j++;
257
+ if (j >= this.n || this.s[j] !== "\n") return false;
258
+ this.i = j + 1;
259
+ this.line++;
260
+ while (this.peek() === " " || this.peek() === "\t" || this.peek() === "\n") {
261
+ if (this.peek() === "\n") this.line++;
262
+ this.i++;
263
+ }
264
+ return true;
265
+ }
266
+
267
+ basicEscape() {
268
+ this.i++; // consume backslash
269
+ const c = this.peek();
270
+ const simple = { b: "\b", t: "\t", n: "\n", f: "\f", r: "\r", '"': '"', "\\": "\\" };
271
+ if (Object.prototype.hasOwnProperty.call(simple, c)) { this.i++; return simple[c]; }
272
+ if (c === "u" || c === "U") {
273
+ this.i++;
274
+ const n = c === "u" ? 4 : 8;
275
+ let hex = "";
276
+ for (let k = 0; k < n; k++) {
277
+ const h = this.peek();
278
+ if (!/[0-9a-fA-F]/.test(h)) this.die("invalid unicode escape");
279
+ hex += h;
280
+ this.i++;
281
+ }
282
+ const cp = parseInt(hex, 16);
283
+ if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) this.die("invalid unicode scalar");
284
+ return String.fromCodePoint(cp);
285
+ }
286
+ this.die("unsupported escape '\\" + (c || "") + "'");
143
287
  }
144
- return out;
145
- }
146
288
 
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;
289
+ // Literal strings are raw. Backslash is not an escape. A single-line literal
290
+ // cannot contain a quote or a newline; a multi-line literal closes on '''.
291
+ parseLiteralString(multi) {
292
+ const delim = multi ? "'''" : "'";
293
+ this.i += delim.length;
294
+ if (multi && this.peek() === "\n") { this.i++; this.line++; }
295
+ let out = "";
296
+ while (!this.eof()) {
297
+ if (!multi && this.peek() === "\n") this.die("unterminated literal string");
298
+ if (this.s.startsWith(delim, this.i)) {
299
+ if (!multi) { this.i++; return out; }
300
+ // One or two quotes may precede the closing three, but never three.
301
+ let q = 3;
302
+ while (this.s[this.i + q] === "'") q++;
303
+ if (q > 5) this.die("too many quotes at end of literal string");
304
+ this.i += q;
305
+ return out + "'".repeat(q - 3);
306
+ }
307
+ const c = this.peek();
308
+ this.rejectControl(c, multi);
309
+ if (c === "\n") this.line++;
310
+ out += c;
311
+ this.i++;
312
+ }
313
+ this.die("unterminated literal string");
314
+ }
315
+
316
+ parseNumber() {
317
+ if (this.peek() !== "+" && this.peek() !== "-") {
318
+ const dated = this.tryDateOrTime();
319
+ if (dated !== null) return dated;
320
+ if (this.s.startsWith("0x", this.i) || this.s.startsWith("0o", this.i) || this.s.startsWith("0b", this.i)) {
321
+ return this.parseRadixInt();
322
+ }
323
+ }
324
+ const special = this.trySpecialFloat();
325
+ if (special !== null) return special;
326
+ const start = this.i;
327
+ const rest = this.s.slice(start);
328
+ const num = /^[+-]?(?:0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/.exec(rest);
329
+ if (!num) this.die("invalid number");
330
+ const raw = num[0];
331
+ if (raw === "+" || raw === "-" || raw.endsWith("_") || raw.includes("__")) this.die("invalid number");
332
+ this.i = start + raw.length;
333
+ if (/[A-Za-z_]/.test(this.peek())) this.die("invalid number");
334
+ const clean = raw.replace(/_/g, "");
335
+ return /[.eE]/.test(clean) ? Number(clean) : this.integer(clean);
336
+ }
337
+
338
+ // inf and nan are lowercase. A sign is allowed. `Inf` is not a float.
339
+ trySpecialFloat() {
340
+ const m = /^[+-]?(inf|nan)(?![A-Za-z0-9_])/.exec(this.s.slice(this.i));
341
+ if (!m) return null;
342
+ this.i += m[0].length;
343
+ if (m[1] === "nan") return NaN;
344
+ return m[0].startsWith("-") ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
345
+ }
346
+
347
+ // Hex, octal and binary have no sign. Underscores sit between digits.
348
+ parseRadixInt() {
349
+ const prefix = this.s.slice(this.i, this.i + 2);
350
+ const base = prefix === "0x" ? 16 : prefix === "0o" ? 8 : prefix === "0b" ? 2 : 0;
351
+ const digit = base === 16 ? /[0-9a-fA-F]/ : base === 8 ? /[0-7]/ : /[01]/;
352
+ this.i += 2;
353
+ if (!digit.test(this.peek())) this.die("invalid number");
354
+ let body = "";
355
+ let prevUnderscore = false;
356
+ while (digit.test(this.peek()) || this.peek() === "_") {
357
+ const c = this.peek();
358
+ if (c === "_") {
359
+ if (prevUnderscore) this.die("invalid number");
360
+ prevUnderscore = true;
361
+ } else {
362
+ prevUnderscore = false;
363
+ body += c;
364
+ }
365
+ this.i++;
366
+ }
367
+ if (prevUnderscore || !body) this.die("invalid number");
368
+ if (/[A-Za-z0-9_]/.test(this.peek())) this.die("invalid number");
369
+ return this.integer(prefix + body);
370
+ }
371
+
372
+ integer(raw) {
373
+ const value = BigInt(raw);
374
+ // Preserve the existing Number API for safe integers; never round int64s.
375
+ return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)
376
+ ? Number(value) : value;
377
+ }
378
+
379
+ // A local date, local time, local date-time, or offset date-time. The
380
+ // lexeme is the value: callers do not interpret it.
381
+ tryDateOrTime() {
382
+ const s = this.s.slice(this.i);
383
+ if (/^\d{2}:\d{2}:\d{2}/.test(s)) return this.readLocalTime();
384
+ if (/^\d{4}-\d{2}-\d{2}/.test(s)) return this.readDate();
385
+ return null;
386
+ }
387
+
388
+ readLocalTime() {
389
+ const m = /^(\d{2}):(\d{2}):(\d{2})(\.\d+)?/.exec(this.s.slice(this.i));
390
+ if (!m) this.die("invalid date or time");
391
+ this.checkTime(m[1], m[2], m[3]);
392
+ this.i += m[0].length;
393
+ this.rejectDateTail();
394
+ return m[0];
395
+ }
396
+
397
+ readDate() {
398
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(this.s.slice(this.i));
399
+ if (!m) this.die("invalid date or time");
400
+ this.checkDate(Number(m[1]), Number(m[2]), Number(m[3]));
401
+ let raw = m[0];
402
+ const rest = this.s.slice(this.i + raw.length);
403
+ if (/^[Tt]/.test(rest) || /^ \d{2}:/.test(rest)) {
404
+ const tm = /^[Tt ](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?/.exec(rest);
405
+ if (!tm) this.die("invalid date or time");
406
+ this.checkTime(tm[1], tm[2], tm[3]);
407
+ if (tm[5] && !/^[Zz]$/.test(tm[5])) this.checkTime(tm[5].slice(1, 3), tm[5].slice(4, 6), "00");
408
+ raw += tm[0];
409
+ }
410
+ this.i += raw.length;
411
+ this.rejectDateTail();
412
+ return raw;
413
+ }
414
+
415
+ rejectDateTail() {
416
+ if (/[0-9A-Za-z_]/.test(this.peek())) this.die("invalid date or time");
417
+ }
418
+
419
+ checkDate(y, m, d) {
420
+ if (y < 1 || m < 1 || m > 12) this.die("invalid date or time");
421
+ const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
422
+ const dim = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
423
+ if (d < 1 || d > dim[m - 1]) this.die("invalid date or time");
424
+ }
425
+
426
+ checkTime(hh, mm, ss) {
427
+ const h = Number(hh);
428
+ const m = Number(mm);
429
+ const s = Number(ss);
430
+ if (h > 23 || m > 59 || s > 59) this.die("invalid date or time");
431
+ }
432
+
433
+ rejectControl(c, multi) {
434
+ if (c === "\n" && multi) return;
435
+ if (c === "\t") return;
436
+ const code = c.charCodeAt(0);
437
+ if (code < 0x20 || code === 0x7f) this.die("control character");
438
+ }
439
+
440
+ // ── scanning ───────────────────────────────────────────────────────────
441
+
442
+ skipSpaces() {
443
+ while (this.peek() === " " || this.peek() === "\t") this.i++;
444
+ }
445
+
446
+ skipIgnorable() {
447
+ for (;;) {
448
+ const c = this.peek();
449
+ if (c === " " || c === "\t") { this.i++; continue; }
450
+ if (c === "\n") { this.i++; this.line++; continue; }
451
+ if (c === "#") {
452
+ this.skipComment();
453
+ continue;
454
+ }
455
+ return;
456
+ }
457
+ }
458
+
459
+ skipComment() {
460
+ while (!this.eof() && this.peek() !== "\n") {
461
+ this.rejectControl(this.peek(), false);
462
+ this.i++;
463
+ }
464
+ }
465
+
466
+ endLine() {
467
+ this.skipSpaces();
468
+ if (this.peek() === "#") this.skipComment();
469
+ if (this.eof()) return;
470
+ if (this.peek() === "\n") { this.i++; this.line++; return; }
471
+ this.die("unexpected trailing content");
472
+ }
473
+
474
+ expect(ch) {
475
+ if (this.peek() !== ch) this.die("expected '" + ch + "'");
476
+ this.i++;
477
+ }
478
+
479
+ peek() { return this.i < this.n ? this.s[this.i] : ""; }
480
+ eof() { return this.i >= this.n; }
481
+
482
+ die(message) {
483
+ const where = this.path ? " in " + this.path : "";
484
+ throw new AgentDocError(CODES.YAML_PARSE, message + " (TOML line " + this.line + where + ")", { path: this.path, line: this.line });
485
+ }
163
486
  }
package/core/yaml.mjs CHANGED
@@ -425,8 +425,10 @@ class Reader {
425
425
  }
426
426
 
427
427
  // Parse a multi-document YAML stream. Returns [{doc, line}] with 1-based line
428
- // numbers of each document root.
429
- export function parseYamlDocuments(src, path) {
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 } = {}) {
430
432
  if (typeof src !== "string") throw new AgentDocError(CODES.UTF8, "expected text for " + path, { path });
431
433
  if (src.charCodeAt(0) === 0xfeff) src = src.slice(1);
432
434
  scanBanned(src, path);
@@ -458,7 +460,7 @@ export function parseYamlDocuments(src, path) {
458
460
  throw new AgentDocError(CODES.YAML_PARSE, "unparsed trailing content", { path, line: r.peek().lineNo });
459
461
  }
460
462
  if (value === null) continue;
461
- if (typeof value !== "object" || Array.isArray(value)) {
463
+ if (typeof value !== "object" || (Array.isArray(value) && !allowSequenceRoot)) {
462
464
  throw new AgentDocError(CODES.YAML_NON_MAP_ROOT, "document root must be a mapping", { path, line: rootLine });
463
465
  }
464
466
  out.push({ doc: value, line: rootLine });