clckernel 1.2.18 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.agents/skills/clckernel_cli/SKILL.md +23 -6
  2. package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
  3. package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
  4. package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
  5. package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
  6. package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
  7. package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
  8. package/.agents/skills/clckernel_cli/reference/start.md +14 -4
  9. package/.agents/skills/clckernel_cli/reference/test.md +1 -1
  10. package/.agents/skills/clckernel_cli/reference/update.md +19 -19
  11. package/CHANGELOG.md +244 -0
  12. package/LICENSE +21 -0
  13. package/README.es.md +280 -51
  14. package/README.md +274 -50
  15. package/bin/cli.js +8 -0
  16. package/package.json +12 -2
  17. package/src/adapters/agent-os.js +161 -84
  18. package/src/adapters/astro.js +8 -6
  19. package/src/adapters/base.js +27 -46
  20. package/src/adapters/django.js +14 -13
  21. package/src/adapters/fastapi.js +16 -15
  22. package/src/adapters/go.js +11 -9
  23. package/src/adapters/laravel.js +12 -10
  24. package/src/adapters/nextjs.js +7 -5
  25. package/src/adapters/rails.js +13 -11
  26. package/src/adapters/rust.js +11 -9
  27. package/src/catalog.js +58 -49
  28. package/src/config.js +38 -240
  29. package/src/detector.js +243 -44
  30. package/src/doctor.js +18 -9
  31. package/src/generator.js +737 -309
  32. package/src/gitignore.js +144 -0
  33. package/src/index.js +257 -18
  34. package/src/io.js +409 -0
  35. package/src/known-hashes.json +309 -0
  36. package/src/onboarding.js +15 -0
  37. package/src/technologies/detector.js +1 -1
  38. package/src/technologies/guards/_shared.js +1 -1
  39. package/src/ui.js +233 -40
  40. package/src/updater.js +271 -36
  41. package/tools/_changes.js +279 -0
  42. package/tools/_changes.py +233 -0
  43. package/tools/_config.js +294 -0
  44. package/tools/_config.py +271 -0
  45. package/tools/_yaml.py +489 -0
  46. package/tools/audit.js +511 -111
  47. package/tools/audit.py +506 -128
  48. package/tools/celery_guard.py +13 -35
  49. package/tools/check_a11y.js +8 -28
  50. package/tools/check_api_contracts.js +13 -39
  51. package/tools/check_architecture.js +35 -44
  52. package/tools/check_architecture.py +172 -105
  53. package/tools/check_custom.js +108 -61
  54. package/tools/check_custom.py +108 -271
  55. package/tools/check_data_files.js +88 -0
  56. package/tools/check_db_efficiency.py +80 -65
  57. package/tools/check_hitl.js +112 -69
  58. package/tools/check_links.js +138 -0
  59. package/tools/check_migrations.py +143 -94
  60. package/tools/check_performance.js +35 -44
  61. package/tools/check_placeholders.js +104 -0
  62. package/tools/check_responsive.js +8 -28
  63. package/tools/check_scope.py +40 -31
  64. package/tools/check_seo.js +8 -28
  65. package/tools/check_skills.js +158 -0
  66. package/tools/check_storybook.js +8 -21
  67. package/tools/check_symlinks.js +40 -22
  68. package/tools/check_ui_reuse.js +17 -29
  69. package/tools/docker_guard.py +19 -41
  70. package/tools/postgres_guard.py +15 -36
  71. package/tools/redis_guard.py +13 -35
  72. package/tools/scan_secrets.js +134 -67
  73. package/tools/scan_secrets.py +163 -72
  74. package/tools/verify_tdd.js +867 -0
  75. package/tools/verify_tdd.py +407 -165
  76. package/tools/yaml.js +404 -132
  77. package/tools/check_domain_data.js +0 -100
  78. package/tools/check_test_calendar_sync.js +0 -111
package/tools/yaml.js CHANGED
@@ -1,173 +1,445 @@
1
1
  /**
2
- * CLC Forge — Zero-Dependency YAML Parser
3
- * Self-contained inline YAML parser extracted from tools/check_custom.js.
4
- * Uses only Node.js built-ins (no npm deps).
2
+ * CLC Kernel — Zero-Dependency strict YAML subset parser.
3
+ * Uses only Node.js built-ins (no npm deps). Twin of `minimal_yaml_parse` in
4
+ * tools/_yaml.py: same functions, same order, same error messages.
5
5
  *
6
- * Supports: scalars, block sequences, block mappings, inline flow
7
- * sequences/mappings, comments. No anchors/aliases, no multiple documents.
6
+ * Supported subset (anything else throws `Unsupported YAML syntax at line N`):
7
+ * - block mappings and block sequences, including a sequence at the SAME
8
+ * indent as its key (`rules:` / `- name:`) and multi-key mapping items
9
+ * - scalars: plain, 'single' (only the '' escape), "double" (escapes
10
+ * \\ \" \/ \n \t \r \0)
11
+ * - types: int, float, true/false (+ True/TRUE/False/FALSE),
12
+ * null/Null/NULL/~/empty; ambiguous YAML 1.1 forms (yes/no/on/off, octal,
13
+ * hex, 1_000, 1:30, dates, .5, .inf, ...) are errors — quote them
14
+ * - flow `[...]` / `{...}` of scalars on one line, quote-aware
15
+ * - comments: `#` at line start or preceded by a space, outside quotes
8
16
  *
9
- * Stack entries: { obj, indent, _parentObj, _parentKey }
10
- * obj: the object we are building key-value pairs into
11
- * indent: indent level (children at same indent stay)
12
- * _parentObj/_parentKey: where to find/create the array for sequences
17
+ * The top level must be a mapping (an empty document parses to {}).
13
18
  *
14
19
  * @module yaml
15
20
  */
16
21
 
22
+ const NULL_WORDS = ['', '~', 'null', 'Null', 'NULL'];
23
+ const TRUE_WORDS = ['true', 'True', 'TRUE'];
24
+ const FALSE_WORDS = ['false', 'False', 'FALSE'];
25
+ const YAML11_BOOL_RE = /^(?:yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF)$/;
26
+ const INT_RE = /^[-+]?(?:0|[1-9][0-9]*)$/;
27
+ const FLOAT_RE = /^[-+]?[0-9]+\.[0-9]+$/;
28
+ // Everything PyYAML would resolve to an int/float/timestamp; used to reject
29
+ // forms outside the subset instead of silently reading them as strings.
30
+ const YAML11_INT_RE = /^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$/;
31
+ const YAML11_FLOAT_RE = /^(?:[-+]?[0-9][0-9_]*\.[0-9_]*(?:[eE][-+][0-9]+)?|\.[0-9_]+(?:[eE][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$/;
32
+ const YAML11_TIMESTAMP_RE = /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(?:$|[Tt \t])/;
33
+ const DOUBLE_ESCAPES = { '\\': '\\', '"': '"', '/': '/', n: '\n', t: '\t', r: '\r', 0: '\0' };
34
+
17
35
  /**
18
- * Detect YAML anchors (`&name`) or aliases (`*name`) on a line.
19
- * @param {string} line - Trimmed content of a YAML line.
20
- * @param {number} lineNo - 1-based line number for error reporting.
21
- * @throws {Error} If anchor or alias syntax is found (unsupported).
36
+ * Build the error thrown for any input outside the subset.
37
+ * @param {number} lineNo - 1-based line number.
38
+ * @param {string} reason - What is not supported.
39
+ * @returns {Error}
22
40
  */
23
- function assertNoAnchorAlias(line, lineNo) {
24
- // YAML anchors/aliases are token-boundary markers: `&name` / `*name`
25
- // preceded by whitespace or start of line. Requiring the boundary avoids
26
- // false positives on regex/glob content such as `pattern: 'a*b'`.
27
- if (/(?:^|\s)[&*][A-Za-z0-9_-]/.test(line)) {
28
- throw new Error(
29
- `Unsupported YAML syntax at line ${lineNo}: anchors/aliases are not supported`
30
- );
41
+ function yamlError(lineNo, reason) {
42
+ return new Error(`Unsupported YAML syntax at line ${lineNo}: ${reason}`);
43
+ }
44
+
45
+ /** Cursor over the significant lines of the document. */
46
+ class Cursor {
47
+ constructor(lines) {
48
+ this.lines = lines;
49
+ this.index = 0;
50
+ }
51
+
52
+ peek() {
53
+ return this.index < this.lines.length ? this.lines[this.index] : null;
54
+ }
55
+
56
+ advance() {
57
+ this.index += 1;
58
+ }
59
+
60
+ /** Replace the current line (used to re-read `- key: v` as a mapping). */
61
+ replace(line) {
62
+ this.lines[this.index] = line;
31
63
  }
32
64
  }
33
65
 
34
66
  /**
35
- * Parse a YAML string into a JavaScript object.
36
- * Throws on anchors/aliases (unsupported).
67
+ * Parse a YAML string (strict subset) into a plain object.
37
68
  * @param {string} text - Raw YAML source.
38
- * @returns {object} Parsed object tree.
69
+ * @returns {object} Parsed mapping ({} for an empty document).
70
+ * @throws {Error} `Unsupported YAML syntax at line N: ...` outside the subset.
39
71
  */
40
72
  function parseYaml(text) {
41
- const lines = text.split('\n');
42
- const root = {};
43
- const stack = [{ obj: root, indent: -1, _parentObj: null, _parentKey: null }];
73
+ const cursor = new Cursor(splitLines(text));
74
+ const first = cursor.peek();
75
+ if (first === null) return {};
76
+ if (isSeqItem(first.text)) throw yamlError(first.lineNo, 'the top level must be a mapping');
77
+ const root = parseMapping(cursor, first.indent);
78
+ const rest = cursor.peek();
79
+ if (rest !== null) throw yamlError(rest.lineNo, 'inconsistent indentation');
80
+ return root;
81
+ }
44
82
 
45
- for (let i = 0; i < lines.length; i++) {
46
- let line = lines[i].replace(/\s*#.*$/, '').trimEnd();
47
- if (line.trim() === '' || line.trim().startsWith('#')) continue;
83
+ /**
84
+ * Split the source into significant lines: { indent, text, lineNo }.
85
+ * Drops blank and comment-only lines; rejects tabs in indentation and
86
+ * document markers.
87
+ */
88
+ function splitLines(text) {
89
+ const lines = [];
90
+ text.split('\n').forEach((raw, i) => {
91
+ const lineNo = i + 1;
92
+ const line = raw.replace(/\r$/, '');
93
+ const content = line.replace(/^[ \t]+/, '');
94
+ if (content === '' || content.startsWith('#')) return;
95
+ const leading = line.slice(0, line.length - content.length);
96
+ if (leading.includes('\t')) throw yamlError(lineNo, 'tabs are not allowed in indentation');
97
+ const body = content.replace(/[ \t]+$/, '');
98
+ if (leading === '' && /^(?:---|\.\.\.)(?:[ \t]|$)/.test(body)) {
99
+ throw yamlError(lineNo, 'multiple documents (---/...) are not supported');
100
+ }
101
+ lines.push({ indent: leading.length, text: body, lineNo });
102
+ });
103
+ return lines;
104
+ }
48
105
 
49
- const indent = line.search(/\S/);
50
- const trimmed = line.trim();
106
+ /** `- item` or a bare `-`. */
107
+ function isSeqItem(text) {
108
+ return text === '-' || text.startsWith('- ');
109
+ }
51
110
 
52
- // New behavior: anchors/aliases are unsupported
53
- assertNoAnchorAlias(trimmed, i + 1);
111
+ /**
112
+ * Parse the block that starts at the current line, whose indent is `indent`.
113
+ * @returns {object|Array}
114
+ */
115
+ function parseBlock(cursor, indent) {
116
+ if (isSeqItem(cursor.peek().text)) return parseSequence(cursor, indent);
117
+ return parseMapping(cursor, indent);
118
+ }
54
119
 
55
- while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
56
- stack.pop();
120
+ /** Parse a block mapping whose keys sit at exactly `indent`. */
121
+ function parseMapping(cursor, indent) {
122
+ const obj = {};
123
+ for (let line = cursor.peek(); line !== null; line = cursor.peek()) {
124
+ if (line.indent < indent) break;
125
+ if (line.indent > indent) throw yamlError(line.lineNo, 'inconsistent indentation');
126
+ if (isSeqItem(line.text)) throw yamlError(line.lineNo, 'sequence item where a mapping key was expected');
127
+ const entry = splitMappingEntry(line.text, line.lineNo);
128
+ if (entry === null) {
129
+ assertNodeStart(line.text, 0, line.lineNo, false);
130
+ throw yamlError(line.lineNo, 'expected "key: value"');
57
131
  }
58
- const entry = stack[stack.length - 1];
59
-
60
- // ── Sequence item: "- rest" ──
61
- if (trimmed.startsWith('- ')) {
62
- const rest = trimmed.slice(2).trim();
63
-
64
- if (rest.includes(': ')) {
65
- // "- key: value" — mapping inside a sequence
66
- let arr = null;
67
- // Check if parent key already points to an array
68
- if (entry._parentObj && entry._parentKey) {
69
- const pk = entry._parentObj[entry._parentKey];
70
- if (Array.isArray(pk)) {
71
- arr = pk;
72
- } else if (pk && typeof pk === 'object' && Object.keys(pk).length === 0) {
73
- // Convert empty object to array
74
- entry._parentObj[entry._parentKey] = [];
75
- arr = entry._parentObj[entry._parentKey];
76
- }
77
- }
78
- if (arr) {
79
- const obj = {};
80
- const colonIdx = rest.indexOf(': ');
81
- obj[rest.slice(0, colonIdx).trim()] = parseScalar(rest.slice(colonIdx + 2).trim());
82
- arr.push(obj);
83
- stack.push({ obj, indent, _parentObj: arr, _parentKey: null });
84
- }
85
- } else if (entry._parentObj && entry._parentKey) {
86
- // "- scalar" inside a sequence
87
- const pk = entry._parentObj[entry._parentKey];
88
- if (Array.isArray(pk)) {
89
- pk.push(parseScalar(rest));
90
- } else if (pk && typeof pk === 'object' && Object.keys(pk).length === 0) {
91
- // Convert empty object to array (same rule as the mapping-in-sequence
92
- // branch above) so plain block sequences like
93
- // active_guards:
94
- // - check_custom
95
- // parse into arrays, not empty objects. Additive: previously valid
96
- // YAML parses identically.
97
- entry._parentObj[entry._parentKey] = [parseScalar(rest)];
98
- }
99
- }
100
- continue;
132
+ if (Object.prototype.hasOwnProperty.call(obj, entry.key)) {
133
+ throw yamlError(line.lineNo, `duplicate key "${entry.key}"`);
101
134
  }
135
+ cursor.advance();
136
+ const inline = parseInlineValue(entry.rest, line.lineNo);
137
+ obj[entry.key] = inline.empty ? parseChild(cursor, indent, true) : inline.value;
138
+ }
139
+ return obj;
140
+ }
102
141
 
103
- // ── key: value pair ──
104
- const kvMatch = trimmed.match(/^([^:]+):\s*(.*)$/);
105
- if (kvMatch) {
106
- const key = kvMatch[1].trim();
107
- const val = kvMatch[2].trim();
108
-
109
- if (val === '' || val === '|' || val === '>') {
110
- // Empty value → children follow
111
- if (!entry.obj[key]) entry.obj[key] = {};
112
- stack.push({ obj: entry.obj[key], indent, _parentObj: entry.obj, _parentKey: key });
113
- } else if (val.startsWith('[') && val.endsWith(']')) {
114
- entry.obj[key] = val.slice(1, -1).split(',').map(s => parseScalar(s.trim()));
115
- } else if (val.startsWith('{') && val.endsWith('}')) {
116
- entry.obj[key] = parseFlowMapping(val);
117
- } else {
118
- entry.obj[key] = parseScalar(val);
119
- }
120
- continue;
142
+ /** Parse a block sequence whose `-` markers sit at exactly `indent`. */
143
+ function parseSequence(cursor, indent) {
144
+ const arr = [];
145
+ for (let line = cursor.peek(); line !== null; line = cursor.peek()) {
146
+ if (line.indent < indent) break;
147
+ if (line.indent > indent) throw yamlError(line.lineNo, 'inconsistent indentation');
148
+ if (!isSeqItem(line.text)) break;
149
+ const rest = line.text.slice(1).replace(/^ +/, '');
150
+ const column = line.indent + (line.text.length - rest.length);
151
+ if (rest === '' || rest.startsWith('#')) {
152
+ cursor.advance();
153
+ arr.push(parseChild(cursor, indent, false));
154
+ } else if (isSeqItem(rest)) {
155
+ throw yamlError(line.lineNo, 'nested sequences on one line are not supported');
156
+ } else if (splitMappingEntry(rest, line.lineNo) !== null) {
157
+ cursor.replace({ indent: column, text: rest, lineNo: line.lineNo });
158
+ arr.push(parseMapping(cursor, column));
159
+ } else {
160
+ cursor.advance();
161
+ arr.push(parseInlineValue(rest, line.lineNo).value);
121
162
  }
122
163
  }
164
+ return arr;
165
+ }
123
166
 
124
- return root;
167
+ /**
168
+ * Value of a `key:` or `-` with nothing after it: a nested block, a sequence
169
+ * at the same indent (only after a key), or null.
170
+ */
171
+ function parseChild(cursor, parentIndent, allowSameIndentSeq) {
172
+ const next = cursor.peek();
173
+ if (next === null) return null;
174
+ if (next.indent > parentIndent) return parseBlock(cursor, next.indent);
175
+ if (allowSameIndentSeq && next.indent === parentIndent && isSeqItem(next.text)) {
176
+ return parseSequence(cursor, parentIndent);
177
+ }
178
+ return null;
125
179
  }
126
180
 
127
- function parseScalar(val) {
128
- if (val === '' || val === '~' || val === 'null') return null;
129
- if (val === 'true') return true;
130
- if (val === 'false') return false;
131
- if (/^-?\d+$/.test(val)) return parseInt(val, 10);
132
- if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
133
- // Strip quotes and unescape common escape sequences
134
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
135
- return val.slice(1, -1).replace(/\\(.)/g, '$1');
181
+ /**
182
+ * Split `key: rest` on the first mapping indicator outside quotes.
183
+ * @returns {{key: string, rest: string}|null} null when the text is not a mapping entry.
184
+ */
185
+ function splitMappingEntry(text, lineNo) {
186
+ const first = text[0];
187
+ if (first === '[' || first === '{') return null;
188
+ if (first === "'" || first === '"') {
189
+ const [key, end] = parseQuoted(text, 0, lineNo);
190
+ const colon = skipSpaces(text, end);
191
+ if (text[colon] === ':' && isBlankOrEnd(text, colon + 1)) {
192
+ return { key, rest: text.slice(colon + 1) };
193
+ }
194
+ return null;
195
+ }
196
+ for (let i = 0; i < text.length; i++) {
197
+ if (text[i] === '#' && i > 0 && text[i - 1] === ' ') return null;
198
+ if (text[i] === ':' && isBlankOrEnd(text, i + 1)) {
199
+ return { key: parsePlainKey(text.slice(0, i).replace(/[ \t]+$/, ''), lineNo), rest: text.slice(i + 1) };
200
+ }
136
201
  }
137
- return val;
202
+ return null;
138
203
  }
139
204
 
140
- function parseFlowMapping(str) {
141
- const inner = str.slice(1, -1).trim();
142
- if (!inner) return {};
143
- const obj = {};
144
- const pairs = inner.split(',');
145
- for (const pair of pairs) {
146
- const colonIdx = pair.indexOf(':');
147
- if (colonIdx === -1) continue;
148
- const k = pair.slice(0, colonIdx).trim();
149
- const v = pair.slice(colonIdx + 1).trim();
150
- obj[k] = parseScalar(v);
205
+ /** Validate a plain (unquoted) mapping key: it must resolve to a string. */
206
+ function parsePlainKey(raw, lineNo) {
207
+ if (raw === '') throw yamlError(lineNo, 'empty mapping key');
208
+ assertNodeStart(raw, 0, lineNo, false);
209
+ if (raw === '<<') throw yamlError(lineNo, 'merge keys (<<) are not supported');
210
+ if (typeof resolvePlain(raw, lineNo) !== 'string') {
211
+ throw yamlError(lineNo, `key "${raw}" is not a string; quote it`);
151
212
  }
152
- return obj;
213
+ return raw;
214
+ }
215
+
216
+ /**
217
+ * Parse what follows `key:` or `- ` on the same line.
218
+ * @returns {{empty: boolean, value: *}} empty when only spaces/comment remain.
219
+ */
220
+ function parseInlineValue(text, lineNo) {
221
+ const start = skipSpaces(text, 0);
222
+ if (start >= text.length || text[start] === '#') return { empty: true, value: null };
223
+ const first = text[start];
224
+ let value;
225
+ let end;
226
+ if (first === '[' || first === '{') {
227
+ [value, end] = parseFlow(text, start, lineNo);
228
+ } else if (first === "'" || first === '"') {
229
+ [value, end] = parseQuoted(text, start, lineNo);
230
+ } else {
231
+ assertNodeStart(text, start, lineNo, false);
232
+ const [raw, stop] = readPlain(text, start, false, lineNo);
233
+ if (raw.includes(': ') || raw.endsWith(':')) {
234
+ throw yamlError(lineNo, 'mapping values are not allowed inside a plain scalar; quote it');
235
+ }
236
+ value = resolvePlain(raw, lineNo);
237
+ end = stop;
238
+ }
239
+ assertEndOfLine(text, end, lineNo);
240
+ return { empty: false, value };
241
+ }
242
+
243
+ /**
244
+ * Parse a one-line flow collection of scalars starting at `pos` (`[` or `{`).
245
+ * @returns {[Array|object, number]} value and the index after the closer.
246
+ */
247
+ function parseFlow(text, pos, lineNo) {
248
+ const isSeq = text[pos] === '[';
249
+ const close = isSeq ? ']' : '}';
250
+ const result = isSeq ? [] : {};
251
+ let i = skipSpaces(text, pos + 1);
252
+ if (text[i] === close) return [result, i + 1];
253
+ for (;;) {
254
+ i = skipSpaces(text, i);
255
+ if (isSeq) {
256
+ let item;
257
+ [item, i] = parseFlowScalar(text, i, lineNo);
258
+ result.push(item);
259
+ } else {
260
+ let key;
261
+ [key, i] = parseFlowScalar(text, i, lineNo, true);
262
+ i = skipSpaces(text, i);
263
+ if (text[i] !== ':') throw yamlError(lineNo, 'expected ":" in flow mapping');
264
+ i = skipSpaces(text, i + 1);
265
+ let value = null;
266
+ if (text[i] !== ',' && text[i] !== close) [value, i] = parseFlowScalar(text, i, lineNo);
267
+ if (Object.prototype.hasOwnProperty.call(result, key)) throw yamlError(lineNo, `duplicate key "${key}"`);
268
+ result[key] = value;
269
+ }
270
+ i = skipSpaces(text, i);
271
+ if (text[i] === ',') {
272
+ i = skipSpaces(text, i + 1);
273
+ if (text[i] === close) return [result, i + 1];
274
+ } else if (text[i] === close) {
275
+ return [result, i + 1];
276
+ } else if (i >= text.length || text[i] === '#') {
277
+ throw yamlError(lineNo, 'unclosed flow collection (flow must fit on one line)');
278
+ } else {
279
+ throw yamlError(lineNo, `expected "," or "${close}" in flow collection`);
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Parse one scalar inside a flow collection.
286
+ * @returns {[*, number]} value and the index after it.
287
+ */
288
+ function parseFlowScalar(text, pos, lineNo, isKey = false) {
289
+ if (pos >= text.length) throw yamlError(lineNo, 'unclosed flow collection (flow must fit on one line)');
290
+ const first = text[pos];
291
+ if (first === '[' || first === '{') throw yamlError(lineNo, 'nested flow collections are not supported');
292
+ if (first === "'" || first === '"') return parseQuoted(text, pos, lineNo);
293
+ assertNodeStart(text, pos, lineNo, true);
294
+ const [raw, end] = readPlain(text, pos, true, lineNo);
295
+ if (raw === '') throw yamlError(lineNo, 'empty entry in flow collection');
296
+ return [isKey ? parsePlainKey(raw, lineNo) : resolvePlain(raw, lineNo), end];
297
+ }
298
+
299
+ /**
300
+ * Parse a single- or double-quoted scalar starting at `pos`.
301
+ * @returns {[string, number]} value and the index after the closing quote.
302
+ */
303
+ function parseQuoted(text, pos, lineNo) {
304
+ const quote = text[pos];
305
+ let out = '';
306
+ let i = pos + 1;
307
+ while (i < text.length) {
308
+ const ch = text[i];
309
+ if (quote === "'" && ch === "'") {
310
+ if (text[i + 1] !== "'") return [out, i + 1];
311
+ out += "'";
312
+ i += 2;
313
+ } else if (quote === '"' && ch === '"') {
314
+ return [out, i + 1];
315
+ } else if (quote === '"' && ch === '\\') {
316
+ const esc = text[i + 1];
317
+ if (esc === undefined) throw yamlError(lineNo, 'unclosed double-quoted scalar');
318
+ if (!Object.prototype.hasOwnProperty.call(DOUBLE_ESCAPES, esc)) {
319
+ throw yamlError(lineNo, `unsupported escape "\\${esc}" in double-quoted scalar`);
320
+ }
321
+ out += DOUBLE_ESCAPES[esc];
322
+ i += 2;
323
+ } else {
324
+ out += ch;
325
+ i += 1;
326
+ }
327
+ }
328
+ throw yamlError(lineNo, `unclosed ${quote === "'" ? 'single' : 'double'}-quoted scalar`);
329
+ }
330
+
331
+ /**
332
+ * Read a plain scalar starting at `pos`; stops at a comment and, inside flow
333
+ * collections, at flow indicators and `: `.
334
+ * @returns {[string, number]} trimmed raw text and the index where it stopped.
335
+ */
336
+ function readPlain(text, pos, inFlow, lineNo) {
337
+ let i = pos;
338
+ while (i < text.length) {
339
+ const ch = text[i];
340
+ if (ch === '#' && text[i - 1] === ' ') break;
341
+ if (ch === '#' && text[i - 1] === '\t') throw yamlError(lineNo, 'tabs are not allowed before comments');
342
+ if (inFlow && ',[]{}?'.includes(ch)) break;
343
+ if (inFlow && ch === ':' && (i + 1 >= text.length || ' \t,[]{}'.includes(text[i + 1]))) break;
344
+ i += 1;
345
+ }
346
+ return [text.slice(pos, i).replace(/[ \t]+$/, ''), i];
347
+ }
348
+
349
+ /** Resolve a plain scalar to null / boolean / number / string. */
350
+ function resolvePlain(raw, lineNo) {
351
+ if (NULL_WORDS.includes(raw)) return null;
352
+ if (TRUE_WORDS.includes(raw)) return true;
353
+ if (FALSE_WORDS.includes(raw)) return false;
354
+ if (YAML11_BOOL_RE.test(raw)) throw yamlError(lineNo, `ambiguous boolean "${raw}"; use true/false or quote it`);
355
+ if (INT_RE.test(raw)) return Number(raw) + 0; // + 0 turns -0 into 0
356
+ if (FLOAT_RE.test(raw)) return Number(raw);
357
+ if (YAML11_INT_RE.test(raw) || YAML11_FLOAT_RE.test(raw) || YAML11_TIMESTAMP_RE.test(raw) || raw === '=' || raw === '<<') {
358
+ throw yamlError(lineNo, `ambiguous scalar "${raw}"; quote it`);
359
+ }
360
+ return raw;
361
+ }
362
+
363
+ /** Reject indicators that cannot start a node in the subset. */
364
+ function assertNodeStart(text, pos, lineNo, inFlow) {
365
+ const ch = text[pos];
366
+ const spaced = isBlankOrEnd(text, pos + 1);
367
+ if (ch === '&' || ch === '*') throw yamlError(lineNo, 'anchors/aliases are not supported');
368
+ if (ch === '!') throw yamlError(lineNo, 'tags (!) are not supported');
369
+ if (ch === '|' || ch === '>') throw yamlError(lineNo, 'block scalars (| and >) are not supported');
370
+ if (ch === '?' && spaced) throw yamlError(lineNo, 'complex keys (? ) are not supported');
371
+ if (ch === '-' && spaced) throw yamlError(lineNo, 'a sequence is not allowed here');
372
+ if (ch === ':' && spaced) throw yamlError(lineNo, 'unexpected ":"');
373
+ if ('%@`,]}#'.includes(ch)) throw yamlError(lineNo, `a plain scalar cannot start with "${ch}"; quote it`);
374
+ if (inFlow && (ch === '[' || ch === '{')) throw yamlError(lineNo, 'nested flow collections are not supported');
375
+ }
376
+
377
+ /** After a value only spaces and a ` #` comment may follow. */
378
+ function assertEndOfLine(text, pos, lineNo) {
379
+ const i = skipSpaces(text, pos);
380
+ if (i >= text.length) return;
381
+ if (text[i] === '#' && text[i - 1] === ' ') return;
382
+ throw yamlError(lineNo, `unexpected content after value: "${text.slice(i)}"`);
383
+ }
384
+
385
+ function skipSpaces(text, pos) {
386
+ let i = pos;
387
+ while (i < text.length && (text[i] === ' ' || text[i] === '\t')) i += 1;
388
+ return i;
389
+ }
390
+
391
+ function isBlankOrEnd(text, pos) {
392
+ return pos >= text.length || text[pos] === ' ' || text[pos] === '\t';
153
393
  }
154
394
 
155
395
  // ═══════════════════════════════════════════════════════════════════════
156
- // Glob-to-Regex Converter (~15 lines)
157
- // Handles: **, *, {a,b} alternation
396
+ // Glob-to-Regex Converter (twin of glob_to_regex in tools/_yaml.py)
397
+ // Handles: **/, trailing **, *, ?, {a,b} alternation; everything else literal
158
398
  // ═══════════════════════════════════════════════════════════════════════
159
399
 
400
+ function escapeRegex(str) {
401
+ return str.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
402
+ }
403
+
404
+ /**
405
+ * Convert a glob pattern to a fully anchored RegExp.
406
+ * - `**\/` matches zero or more leading directories
407
+ * - `**` (trailing or standalone) matches anything, separators included
408
+ * - `*` matches any run of characters except `/`
409
+ * - `?` matches exactly one character except `/`
410
+ * - `{a,b}` matches either alternative (alternatives are literal)
411
+ * - every other character is matched literally
412
+ * @param {string} glob
413
+ * @returns {RegExp}
414
+ */
160
415
  function globToRegex(glob) {
161
- let re = glob
162
- .replace(/\./g, '\\.') // escape dots
163
- .replace(/\*\*\//g, '{{GLOBSTAR_SLASH}}') // protect **/ (zero or more segments + separator)
164
- .replace(/\*\*/g, '{{GLOBSTAR}}') // protect trailing **
165
- .replace(/\*/g, '[^/]*') // single * = no slashes
166
- .replace(/\{\{GLOBSTAR_SLASH\}\}/g, '(?:.+/)?') // **/ = optional path segments
167
- .replace(/\{\{GLOBSTAR\}\}/g, '.*') // ** = anything
168
- .replace(/\{([^}]+)\}/g, (_, alt) =>
169
- `(${alt.split(',').join('|')})`); // {a,b} alternation
170
- return new RegExp('^' + re + '$');
416
+ const out = [];
417
+ let i = 0;
418
+ while (i < glob.length) {
419
+ const ch = glob[i];
420
+ if (glob.startsWith('**/', i)) {
421
+ out.push('(?:.+/)?');
422
+ i += 3;
423
+ } else if (glob.startsWith('**', i)) {
424
+ out.push('.*');
425
+ i += 2;
426
+ } else if (ch === '*') {
427
+ out.push('[^/]*');
428
+ i += 1;
429
+ } else if (ch === '?') {
430
+ out.push('[^/]');
431
+ i += 1;
432
+ } else if (ch === '{' && glob.indexOf('}', i + 1) !== -1) {
433
+ const close = glob.indexOf('}', i + 1);
434
+ const alternatives = glob.slice(i + 1, close).split(',');
435
+ out.push('(?:' + alternatives.map(escapeRegex).join('|') + ')');
436
+ i = close + 1;
437
+ } else {
438
+ out.push(escapeRegex(ch));
439
+ i += 1;
440
+ }
441
+ }
442
+ return new RegExp('^' + out.join('') + '$');
171
443
  }
172
444
 
173
445
  module.exports = { parseYaml, globToRegex };