agent-doc-system 1.1.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.
- package/CHANGELOG.md +269 -0
- package/README.md +38 -0
- package/adapters/ansible.mjs +1 -1
- package/adapters/cache.mjs +8 -2
- package/adapters/contract-consumers.mjs +1 -1
- package/adapters/database.mjs +18 -4
- package/adapters/events.mjs +1 -1
- package/adapters/github-actions.mjs +244 -85
- package/adapters/go.mjs +1 -1
- package/adapters/index.mjs +3 -1
- package/adapters/object-store.mjs +2 -2
- package/adapters/python.mjs +1142 -0
- package/adapters/resource-client.mjs +1 -1
- package/adapters/rust.mjs +987 -0
- package/adapters/typescript.mjs +1 -1
- package/bin/agentdoc.mjs +2 -2
- package/core/audit.mjs +5 -3
- package/core/codes.mjs +4 -0
- package/core/compile.mjs +124 -11
- package/core/dependents.mjs +42 -0
- package/core/discover.mjs +60 -9
- package/core/fsx.mjs +17 -0
- package/core/graph.mjs +1 -1
- package/core/impact.mjs +55 -44
- package/core/query.mjs +18 -8
- package/core/scaffold.mjs +87 -22
- package/core/sourcescan.mjs +11 -2
- package/core/toml.mjs +463 -140
- package/docs/ADAPTERS.md +196 -6
- package/docs/BENCHMARK.md +68 -0
- package/docs/CI.md +1 -1
- package/docs/EVALS.md +33 -2
- package/docs/MIGRATION.md +4 -2
- package/docs/QUERY.md +53 -4
- package/docs/SPEC.md +22 -0
- package/docs/ci/github-actions.yml +1 -1
- package/evals/harness.mjs +32 -6
- package/evals/scenarios/fixture-b-03-change-shared-lib.json +8 -1
- package/evals/scenarios/fixture-c-03-shared-vocabulary.json +7 -1
- package/evals/scenarios/fixture-e-01-change-shared-lib.json +14 -0
- package/evals/scenarios/fixture-e-02-change-manifest.json +9 -0
- package/evals/scenarios/fixture-e-03-local-module.json +9 -0
- package/evals/scenarios/fixture-e-04-ambiguous-crate.json +14 -0
- package/evals/scenarios/fixture-f-01-change-shared-lib.json +9 -0
- package/evals/scenarios/fixture-f-02-change-manifest.json +9 -0
- package/evals/scenarios/fixture-f-03-shadowed-import.json +9 -0
- package/evals/scenarios/fixture-f-04-nested-workspace.json +9 -0
- package/evals/scenarios/fixture-f-05-alembic-database.json +9 -0
- package/evals/scenarios/fixture-f-06-unittest-job.json +9 -0
- package/package.json +2 -1
- package/skills/agent-doc-system/references/create-migrate.md +13 -3
- package/templates/ci.yml +1 -1
package/core/toml.mjs
CHANGED
|
@@ -1,163 +1,486 @@
|
|
|
1
|
-
//
|
|
1
|
+
// TOML reader for deployment manifests and Python project files.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
130
|
+
return value;
|
|
54
131
|
}
|
|
55
|
-
return root;
|
|
56
|
-
}
|
|
57
132
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
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
|
-
|
|
94
|
-
return s.split(".").map((k) => unquoteKey(k.trim()));
|
|
95
|
-
}
|
|
163
|
+
// ── values ─────────────────────────────────────────────────────────────
|
|
96
164
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|