typeglish 0.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.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +13 -0
  3. package/README.md +301 -0
  4. package/dist/chunk-3NQ3VYO6.js +566 -0
  5. package/dist/chunk-3NQ3VYO6.js.map +1 -0
  6. package/dist/chunk-KRYC4OTL.js +3573 -0
  7. package/dist/chunk-KRYC4OTL.js.map +1 -0
  8. package/dist/chunk-NLQIL5WD.js +25 -0
  9. package/dist/chunk-NLQIL5WD.js.map +1 -0
  10. package/dist/chunk-PR4QN5HX.js +39 -0
  11. package/dist/chunk-PR4QN5HX.js.map +1 -0
  12. package/dist/chunk-PXAMTGYY.js +3 -0
  13. package/dist/chunk-PXAMTGYY.js.map +1 -0
  14. package/dist/chunk-VMACILYN.js +140 -0
  15. package/dist/chunk-VMACILYN.js.map +1 -0
  16. package/dist/chunk-Y64EAEYB.js +10919 -0
  17. package/dist/chunk-Y64EAEYB.js.map +1 -0
  18. package/dist/chunk-YH4ZLQ4G.js +4260 -0
  19. package/dist/chunk-YH4ZLQ4G.js.map +1 -0
  20. package/dist/chunk-ZKMHZHID.js +56 -0
  21. package/dist/chunk-ZKMHZHID.js.map +1 -0
  22. package/dist/cli.d.ts +1 -0
  23. package/dist/cli.js +447 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/compile-DViQS2oG.d.ts +310 -0
  26. package/dist/diagnostics-BWJ_oMd8.d.ts +49 -0
  27. package/dist/exhaustiveness-VBVWCX5W.js +123 -0
  28. package/dist/exhaustiveness-VBVWCX5W.js.map +1 -0
  29. package/dist/index-Df_QnTgq.d.ts +313 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +7 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/mcp.d.ts +11 -0
  34. package/dist/mcp.js +36378 -0
  35. package/dist/mcp.js.map +1 -0
  36. package/dist/monaco.css +46 -0
  37. package/dist/monaco.d.ts +43 -0
  38. package/dist/monaco.js +971 -0
  39. package/dist/monaco.js.map +1 -0
  40. package/dist/node.d.ts +21 -0
  41. package/dist/node.js +8 -0
  42. package/dist/node.js.map +1 -0
  43. package/dist/spell-node.d.ts +8 -0
  44. package/dist/spell-node.js +23 -0
  45. package/dist/spell-node.js.map +1 -0
  46. package/dist/spell.d.ts +18 -0
  47. package/dist/spell.js +4 -0
  48. package/dist/spell.js.map +1 -0
  49. package/dist/z3-MWU3GSVT.js +5 -0
  50. package/dist/z3-MWU3GSVT.js.map +1 -0
  51. package/package.json +160 -0
@@ -0,0 +1,4260 @@
1
+ import { COMPARISON_KEYWORDS, COMPARISON_SYMBOLS, CARDINAL_QUANTITIES, EXACTNESS_MARKERS, QUANTIFIER_KEYWORDS, copulaAlternation, examplesMask, configSectionMask, toolBlockMask, stripPunct, stripInlineComment, splitSentences, stripAnnotation, maskQuotes, CONFIG_RE, TRANSCLUSION_LINE_RE, IMPORT_FILE_RE, REQUIRE_RE, IMPORT_RE, DEFINE_RE, CONFIG_SECTION_OPEN, CONFIG_SECTION_CLOSE, annotationAlias, isAnnotationLine, TAG_NAME_SRC, SNAKE_RE, BARE_COMMAND_RE, NUMBER_WORDS, NAME_PHRASE, bindsHere, toolsFromLines, literalSectionMask, defListMask, defEntries, renderDefBlock, stripQuotes, HONORIFIC_PREFIX_SRC, NAME_WORD, copulaConnector, SERVICE_DECL_RE, TOOL_DECL_RE } from './chunk-Y64EAEYB.js';
2
+ import { __commonJS, __require, __toESM } from './chunk-PR4QN5HX.js';
3
+
4
+ // node_modules/pluralize/pluralize.js
5
+ var require_pluralize = __commonJS({
6
+ "node_modules/pluralize/pluralize.js"(exports, module) {
7
+ (function(root, pluralize) {
8
+ if (typeof __require === "function" && typeof exports === "object" && typeof module === "object") {
9
+ module.exports = pluralize();
10
+ } else if (typeof define === "function" && define.amd) {
11
+ define(function() {
12
+ return pluralize();
13
+ });
14
+ } else {
15
+ root.pluralize = pluralize();
16
+ }
17
+ })(exports, function() {
18
+ var pluralRules = [];
19
+ var singularRules = [];
20
+ var uncountables = {};
21
+ var irregularPlurals = {};
22
+ var irregularSingles = {};
23
+ function sanitizeRule(rule) {
24
+ if (typeof rule === "string") {
25
+ return new RegExp("^" + rule + "$", "i");
26
+ }
27
+ return rule;
28
+ }
29
+ function restoreCase(word, token) {
30
+ if (word === token) return token;
31
+ if (word === word.toLowerCase()) return token.toLowerCase();
32
+ if (word === word.toUpperCase()) return token.toUpperCase();
33
+ if (word[0] === word[0].toUpperCase()) {
34
+ return token.charAt(0).toUpperCase() + token.substr(1).toLowerCase();
35
+ }
36
+ return token.toLowerCase();
37
+ }
38
+ function interpolate(str, args) {
39
+ return str.replace(/\$(\d{1,2})/g, function(match, index) {
40
+ return args[index] || "";
41
+ });
42
+ }
43
+ function replace(word, rule) {
44
+ return word.replace(rule[0], function(match, index) {
45
+ var result = interpolate(rule[1], arguments);
46
+ if (match === "") {
47
+ return restoreCase(word[index - 1], result);
48
+ }
49
+ return restoreCase(match, result);
50
+ });
51
+ }
52
+ function sanitizeWord(token, word, rules) {
53
+ if (!token.length || uncountables.hasOwnProperty(token)) {
54
+ return word;
55
+ }
56
+ var len = rules.length;
57
+ while (len--) {
58
+ var rule = rules[len];
59
+ if (rule[0].test(word)) return replace(word, rule);
60
+ }
61
+ return word;
62
+ }
63
+ function replaceWord(replaceMap, keepMap, rules) {
64
+ return function(word) {
65
+ var token = word.toLowerCase();
66
+ if (keepMap.hasOwnProperty(token)) {
67
+ return restoreCase(word, token);
68
+ }
69
+ if (replaceMap.hasOwnProperty(token)) {
70
+ return restoreCase(word, replaceMap[token]);
71
+ }
72
+ return sanitizeWord(token, word, rules);
73
+ };
74
+ }
75
+ function checkWord(replaceMap, keepMap, rules, bool) {
76
+ return function(word) {
77
+ var token = word.toLowerCase();
78
+ if (keepMap.hasOwnProperty(token)) return true;
79
+ if (replaceMap.hasOwnProperty(token)) return false;
80
+ return sanitizeWord(token, token, rules) === token;
81
+ };
82
+ }
83
+ function pluralize(word, count, inclusive) {
84
+ var pluralized = count === 1 ? pluralize.singular(word) : pluralize.plural(word);
85
+ return (inclusive ? count + " " : "") + pluralized;
86
+ }
87
+ pluralize.plural = replaceWord(
88
+ irregularSingles,
89
+ irregularPlurals,
90
+ pluralRules
91
+ );
92
+ pluralize.isPlural = checkWord(
93
+ irregularSingles,
94
+ irregularPlurals,
95
+ pluralRules
96
+ );
97
+ pluralize.singular = replaceWord(
98
+ irregularPlurals,
99
+ irregularSingles,
100
+ singularRules
101
+ );
102
+ pluralize.isSingular = checkWord(
103
+ irregularPlurals,
104
+ irregularSingles,
105
+ singularRules
106
+ );
107
+ pluralize.addPluralRule = function(rule, replacement) {
108
+ pluralRules.push([sanitizeRule(rule), replacement]);
109
+ };
110
+ pluralize.addSingularRule = function(rule, replacement) {
111
+ singularRules.push([sanitizeRule(rule), replacement]);
112
+ };
113
+ pluralize.addUncountableRule = function(word) {
114
+ if (typeof word === "string") {
115
+ uncountables[word.toLowerCase()] = true;
116
+ return;
117
+ }
118
+ pluralize.addPluralRule(word, "$0");
119
+ pluralize.addSingularRule(word, "$0");
120
+ };
121
+ pluralize.addIrregularRule = function(single, plural) {
122
+ plural = plural.toLowerCase();
123
+ single = single.toLowerCase();
124
+ irregularSingles[single] = plural;
125
+ irregularPlurals[plural] = single;
126
+ };
127
+ [
128
+ // Pronouns.
129
+ ["I", "we"],
130
+ ["me", "us"],
131
+ ["he", "they"],
132
+ ["she", "they"],
133
+ ["them", "them"],
134
+ ["myself", "ourselves"],
135
+ ["yourself", "yourselves"],
136
+ ["itself", "themselves"],
137
+ ["herself", "themselves"],
138
+ ["himself", "themselves"],
139
+ ["themself", "themselves"],
140
+ ["is", "are"],
141
+ ["was", "were"],
142
+ ["has", "have"],
143
+ ["this", "these"],
144
+ ["that", "those"],
145
+ // Words ending in with a consonant and `o`.
146
+ ["echo", "echoes"],
147
+ ["dingo", "dingoes"],
148
+ ["volcano", "volcanoes"],
149
+ ["tornado", "tornadoes"],
150
+ ["torpedo", "torpedoes"],
151
+ // Ends with `us`.
152
+ ["genus", "genera"],
153
+ ["viscus", "viscera"],
154
+ // Ends with `ma`.
155
+ ["stigma", "stigmata"],
156
+ ["stoma", "stomata"],
157
+ ["dogma", "dogmata"],
158
+ ["lemma", "lemmata"],
159
+ ["schema", "schemata"],
160
+ ["anathema", "anathemata"],
161
+ // Other irregular rules.
162
+ ["ox", "oxen"],
163
+ ["axe", "axes"],
164
+ ["die", "dice"],
165
+ ["yes", "yeses"],
166
+ ["foot", "feet"],
167
+ ["eave", "eaves"],
168
+ ["goose", "geese"],
169
+ ["tooth", "teeth"],
170
+ ["quiz", "quizzes"],
171
+ ["human", "humans"],
172
+ ["proof", "proofs"],
173
+ ["carve", "carves"],
174
+ ["valve", "valves"],
175
+ ["looey", "looies"],
176
+ ["thief", "thieves"],
177
+ ["groove", "grooves"],
178
+ ["pickaxe", "pickaxes"],
179
+ ["passerby", "passersby"]
180
+ ].forEach(function(rule) {
181
+ return pluralize.addIrregularRule(rule[0], rule[1]);
182
+ });
183
+ [
184
+ [/s?$/i, "s"],
185
+ [/[^\u0000-\u007F]$/i, "$0"],
186
+ [/([^aeiou]ese)$/i, "$1"],
187
+ [/(ax|test)is$/i, "$1es"],
188
+ [/(alias|[^aou]us|t[lm]as|gas|ris)$/i, "$1es"],
189
+ [/(e[mn]u)s?$/i, "$1s"],
190
+ [/([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$/i, "$1"],
191
+ [/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, "$1i"],
192
+ [/(alumn|alg|vertebr)(?:a|ae)$/i, "$1ae"],
193
+ [/(seraph|cherub)(?:im)?$/i, "$1im"],
194
+ [/(her|at|gr)o$/i, "$1oes"],
195
+ [/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i, "$1a"],
196
+ [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i, "$1a"],
197
+ [/sis$/i, "ses"],
198
+ [/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, "$1$2ves"],
199
+ [/([^aeiouy]|qu)y$/i, "$1ies"],
200
+ [/([^ch][ieo][ln])ey$/i, "$1ies"],
201
+ [/(x|ch|ss|sh|zz)$/i, "$1es"],
202
+ [/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, "$1ices"],
203
+ [/\b((?:tit)?m|l)(?:ice|ouse)$/i, "$1ice"],
204
+ [/(pe)(?:rson|ople)$/i, "$1ople"],
205
+ [/(child)(?:ren)?$/i, "$1ren"],
206
+ [/eaux$/i, "$0"],
207
+ [/m[ae]n$/i, "men"],
208
+ ["thou", "you"]
209
+ ].forEach(function(rule) {
210
+ return pluralize.addPluralRule(rule[0], rule[1]);
211
+ });
212
+ [
213
+ [/s$/i, ""],
214
+ [/(ss)$/i, "$1"],
215
+ [/(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i, "$1fe"],
216
+ [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, "$1f"],
217
+ [/ies$/i, "y"],
218
+ [/\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i, "$1ie"],
219
+ [/\b(mon|smil)ies$/i, "$1ey"],
220
+ [/\b((?:tit)?m|l)ice$/i, "$1ouse"],
221
+ [/(seraph|cherub)im$/i, "$1"],
222
+ [/(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i, "$1"],
223
+ [/(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i, "$1sis"],
224
+ [/(movie|twelve|abuse|e[mn]u)s$/i, "$1"],
225
+ [/(test)(?:is|es)$/i, "$1is"],
226
+ [/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, "$1us"],
227
+ [/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i, "$1um"],
228
+ [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i, "$1on"],
229
+ [/(alumn|alg|vertebr)ae$/i, "$1a"],
230
+ [/(cod|mur|sil|vert|ind)ices$/i, "$1ex"],
231
+ [/(matr|append)ices$/i, "$1ix"],
232
+ [/(pe)(rson|ople)$/i, "$1rson"],
233
+ [/(child)ren$/i, "$1"],
234
+ [/(eau)x?$/i, "$1"],
235
+ [/men$/i, "man"]
236
+ ].forEach(function(rule) {
237
+ return pluralize.addSingularRule(rule[0], rule[1]);
238
+ });
239
+ [
240
+ // Singular words with no plurals.
241
+ "adulthood",
242
+ "advice",
243
+ "agenda",
244
+ "aid",
245
+ "aircraft",
246
+ "alcohol",
247
+ "ammo",
248
+ "analytics",
249
+ "anime",
250
+ "athletics",
251
+ "audio",
252
+ "bison",
253
+ "blood",
254
+ "bream",
255
+ "buffalo",
256
+ "butter",
257
+ "carp",
258
+ "cash",
259
+ "chassis",
260
+ "chess",
261
+ "clothing",
262
+ "cod",
263
+ "commerce",
264
+ "cooperation",
265
+ "corps",
266
+ "debris",
267
+ "diabetes",
268
+ "digestion",
269
+ "elk",
270
+ "energy",
271
+ "equipment",
272
+ "excretion",
273
+ "expertise",
274
+ "firmware",
275
+ "flounder",
276
+ "fun",
277
+ "gallows",
278
+ "garbage",
279
+ "graffiti",
280
+ "hardware",
281
+ "headquarters",
282
+ "health",
283
+ "herpes",
284
+ "highjinks",
285
+ "homework",
286
+ "housework",
287
+ "information",
288
+ "jeans",
289
+ "justice",
290
+ "kudos",
291
+ "labour",
292
+ "literature",
293
+ "machinery",
294
+ "mackerel",
295
+ "mail",
296
+ "media",
297
+ "mews",
298
+ "moose",
299
+ "music",
300
+ "mud",
301
+ "manga",
302
+ "news",
303
+ "only",
304
+ "personnel",
305
+ "pike",
306
+ "plankton",
307
+ "pliers",
308
+ "police",
309
+ "pollution",
310
+ "premises",
311
+ "rain",
312
+ "research",
313
+ "rice",
314
+ "salmon",
315
+ "scissors",
316
+ "series",
317
+ "sewage",
318
+ "shambles",
319
+ "shrimp",
320
+ "software",
321
+ "species",
322
+ "staff",
323
+ "swine",
324
+ "tennis",
325
+ "traffic",
326
+ "transportation",
327
+ "trout",
328
+ "tuna",
329
+ "wealth",
330
+ "welfare",
331
+ "whiting",
332
+ "wildebeest",
333
+ "wildlife",
334
+ "you",
335
+ /pok[eé]mon$/i,
336
+ // Regexes.
337
+ /[^aeiou]ese$/i,
338
+ // "chinese", "japanese"
339
+ /deer$/i,
340
+ // "deer", "reindeer"
341
+ /fish$/i,
342
+ // "fish", "blowfish", "angelfish"
343
+ /measles$/i,
344
+ /o[iu]s$/i,
345
+ // "carnivorous"
346
+ /pox$/i,
347
+ // "chickpox", "smallpox"
348
+ /sheep$/i
349
+ ].forEach(pluralize.addUncountableRule);
350
+ return pluralize;
351
+ });
352
+ }
353
+ });
354
+
355
+ // src/core/expr.ts
356
+ var KEYWORDS = /* @__PURE__ */ new Set(["not", "and", "or", ...COMPARISON_KEYWORDS.flatMap((p) => p.toLowerCase().split(" "))]);
357
+ var TOKEN = /@\{([a-z][a-z0-9_]*)\}|\$([a-z][a-z0-9_]*)|([a-z][a-z0-9_]*)|(,)|(\()|(\))|(-?\d+(?:\.\d+)?)|(>=|<=|>|<|==|!=|=)/gi;
358
+ function pointerRewrite(text) {
359
+ return text.replace(/\$([a-z][a-z0-9_]*)/gi, (_, n) => `@{${n.toLowerCase()}}`);
360
+ }
361
+ var SYMBOL_REWRITE = Object.fromEntries(
362
+ COMPARISON_SYMBOLS.flatMap((c) => c.symbols.map((s) => [s, c.machine]))
363
+ );
364
+ function tokenize(s) {
365
+ const toks = [];
366
+ TOKEN.lastIndex = 0;
367
+ let m;
368
+ while ((m = TOKEN.exec(s)) !== null) {
369
+ if (m[1] !== void 0) toks.push({ t: "var", v: m[1].toLowerCase() });
370
+ else if (m[2] !== void 0) toks.push({ t: "legacyvar", v: m[2].toLowerCase() });
371
+ else if (m[3] !== void 0) {
372
+ const w = m[3].toLowerCase();
373
+ toks.push(KEYWORDS.has(w) ? { t: "kw", v: w } : { t: "word", v: w });
374
+ } else if (m[4] !== void 0) toks.push({ t: "comma" });
375
+ else if (m[5] !== void 0) toks.push({ t: "lp" });
376
+ else if (m[6] !== void 0) toks.push({ t: "rp" });
377
+ else if (m[7] !== void 0) toks.push({ t: "num", v: Number(m[7]) });
378
+ else if (m[8] !== void 0) toks.push({ t: "op", v: m[8] });
379
+ }
380
+ return toks;
381
+ }
382
+ function parseExpr(text) {
383
+ const toks = tokenize(text);
384
+ if (toks.length === 0) return { ast: null, error: "empty condition" };
385
+ let i = 0;
386
+ let error = null;
387
+ const peek = () => toks[i];
388
+ const kw = (v) => peek()?.t === "kw" && peek().v === v;
389
+ const fail = (msg) => {
390
+ if (error === null) error = msg;
391
+ return null;
392
+ };
393
+ const parseValue = () => {
394
+ const tk = peek();
395
+ if (tk?.t === "word") {
396
+ i += 1;
397
+ return tk.v;
398
+ }
399
+ if (tk?.t === "num") {
400
+ i += 1;
401
+ return String(tk.v);
402
+ }
403
+ return fail("expected a value (a plain word or number, e.g. `booking`)");
404
+ };
405
+ const parseNumber = (op, name) => {
406
+ const tk = peek();
407
+ if (tk?.t === "num") {
408
+ i += 1;
409
+ return tk.v;
410
+ }
411
+ fail(`\`is ${op}\` compares numbers \u2014 write \`@{${name}} is ${op} 100\`. For a value match, write \`@{${name}} is equal to \u2026\`.`);
412
+ return null;
413
+ };
414
+ const parseAtom = () => {
415
+ const tk = peek();
416
+ if (tk?.t === "lp") {
417
+ i += 1;
418
+ const inner = parseOr();
419
+ if (!inner) return null;
420
+ if (peek()?.t !== "rp") return fail("missing `)`");
421
+ i += 1;
422
+ return inner;
423
+ }
424
+ if (tk?.t === "legacyvar") {
425
+ return fail(`a variable in a condition is a pointer \u2014 \`@{${tk.v}}\`, never \`$${tk.v}\`. Write the condition as: \`${pointerRewrite(text)}\`.`);
426
+ }
427
+ if (tk?.t !== "var") return fail("expected a `@{variable}` pointer");
428
+ i += 1;
429
+ const name = tk.v;
430
+ const opTok = peek();
431
+ if (opTok?.t === "op") {
432
+ i += 1;
433
+ const num = peek();
434
+ const operand = num?.t === "num" ? ` ${num.v}` : num?.t === "word" ? ` ${num.v}` : " \u2026";
435
+ return fail(`comparisons are English \u2014 \`${opTok.v}\` is not an operator. Write \`@{${name}} ${SYMBOL_REWRITE[opTok.v] ?? "is equal to"}${operand}\`.`);
436
+ }
437
+ if (!kw("is")) return { kind: "read", name };
438
+ i += 1;
439
+ const negate = kw("not");
440
+ if (negate) i += 1;
441
+ const wrap = (e) => negate ? { kind: "not", arg: e } : e;
442
+ if (kw("equal")) {
443
+ i += 1;
444
+ if (!kw("to")) return fail("expected `to` after `equal` \u2014 the operator is `is equal to`");
445
+ i += 1;
446
+ const value = parseValue();
447
+ return value === null ? null : { kind: "is", name, value, negate };
448
+ }
449
+ if (kw("greater")) {
450
+ i += 1;
451
+ if (!kw("than")) return fail("expected `than` after `greater` \u2014 the operator is `is greater than`");
452
+ i += 1;
453
+ const n = parseNumber("greater than", name);
454
+ return n === null ? null : wrap({ kind: "cmp", name, op: ">", n });
455
+ }
456
+ if (kw("less")) {
457
+ i += 1;
458
+ if (!kw("than")) return fail("expected `than` after `less` \u2014 the operator is `is less than`");
459
+ i += 1;
460
+ const n = parseNumber("less than", name);
461
+ return n === null ? null : wrap({ kind: "cmp", name, op: "<", n });
462
+ }
463
+ if (kw("at")) {
464
+ i += 1;
465
+ const bound = kw("least") ? "least" : kw("most") ? "most" : null;
466
+ if (bound === null) return fail("expected `least` or `most` after `at` \u2014 the operators are `is at least` / `is at most`");
467
+ i += 1;
468
+ const n = parseNumber(`at ${bound}`, name);
469
+ if (n === null) return null;
470
+ if (negate) {
471
+ const positive = bound === "least" ? `is less than ${n}` : `is greater than ${n}`;
472
+ return fail(`\`is not at ${bound}\` has a positive form \u2014 write \`@{${name}} ${positive}\`.`);
473
+ }
474
+ return { kind: "cmp", name, op: bound === "least" ? ">=" : "<=", n };
475
+ }
476
+ if (kw("one")) {
477
+ i += 1;
478
+ if (!kw("of")) return fail("expected `of` after `one` \u2014 the operator is `is one of`");
479
+ i += 1;
480
+ const values = [];
481
+ const first = parseValue();
482
+ if (first === null) return null;
483
+ values.push(first);
484
+ while (peek()?.t === "comma") {
485
+ i += 1;
486
+ const v = parseValue();
487
+ if (v === null) return null;
488
+ values.push(v);
489
+ }
490
+ return wrap({ kind: "oneOf", name, values });
491
+ }
492
+ const vTok = peek();
493
+ const shown = vTok?.t === "word" ? vTok.v : vTok?.t === "num" ? String(vTok.v) : "\u2026";
494
+ return fail(`equality is written \`is equal to\` \u2014 write \`@{${name}} is ${negate ? "not " : ""}equal to ${shown}\` (every comparison reads \`is [not] <operator> <operand>\`).`);
495
+ };
496
+ const parseUnary = () => {
497
+ if (kw("not")) {
498
+ i += 1;
499
+ const arg = parseUnary();
500
+ return arg === null ? null : { kind: "not", arg };
501
+ }
502
+ return parseAtom();
503
+ };
504
+ const parseAnd = () => {
505
+ let left = parseUnary();
506
+ if (!left) return null;
507
+ while (kw("and")) {
508
+ i += 1;
509
+ const right = parseUnary();
510
+ if (!right) return null;
511
+ left = { kind: "and", left, right };
512
+ }
513
+ return left;
514
+ };
515
+ function parseOr() {
516
+ let left = parseAnd();
517
+ if (!left) return null;
518
+ while (kw("or")) {
519
+ i += 1;
520
+ const right = parseAnd();
521
+ if (!right) return null;
522
+ left = { kind: "or", left, right };
523
+ }
524
+ return left;
525
+ }
526
+ const ast = parseOr();
527
+ if (error !== null) return { ast: null, error };
528
+ if (i < toks.length) {
529
+ const tk = toks[i];
530
+ const shown = tk === void 0 ? "end" : tk.t === "var" ? `@{${tk.v}}` : tk.t === "legacyvar" ? `$${tk.v}` : "v" in tk ? tk.v : tk.t;
531
+ return { ast: null, error: `unexpected \`${shown}\`` };
532
+ }
533
+ return { ast, error: null };
534
+ }
535
+ var norm = (v) => String(v ?? "").trim().toLowerCase();
536
+ function evalExpr(e, bag) {
537
+ switch (e.kind) {
538
+ case "read": {
539
+ const v = bag[e.name];
540
+ return v === true || norm(v) === "true";
541
+ }
542
+ case "not":
543
+ return !evalExpr(e.arg, bag);
544
+ case "and":
545
+ return evalExpr(e.left, bag) && evalExpr(e.right, bag);
546
+ case "or":
547
+ return evalExpr(e.left, bag) || evalExpr(e.right, bag);
548
+ case "is": {
549
+ const hit = norm(bag[e.name]) === norm(e.value);
550
+ return e.negate ? !hit : hit;
551
+ }
552
+ case "oneOf": {
553
+ const v = norm(bag[e.name]);
554
+ return e.values.some((x) => norm(x) === v);
555
+ }
556
+ case "cmp": {
557
+ const v = Number(bag[e.name]);
558
+ if (Number.isNaN(v)) return false;
559
+ return e.op === ">" ? v > e.n : e.op === ">=" ? v >= e.n : e.op === "<" ? v < e.n : v <= e.n;
560
+ }
561
+ }
562
+ }
563
+ function exprAtoms(e) {
564
+ switch (e.kind) {
565
+ case "read":
566
+ return [{ name: e.name, test: "bool", values: [] }];
567
+ case "is":
568
+ return [{ name: e.name, test: "value", values: [e.value] }];
569
+ case "oneOf":
570
+ return [{ name: e.name, test: "value", values: e.values }];
571
+ case "cmp":
572
+ return [{ name: e.name, test: "numeric", values: [] }];
573
+ case "not":
574
+ return exprAtoms(e.arg);
575
+ case "and":
576
+ case "or":
577
+ return [...exprAtoms(e.left), ...exprAtoms(e.right)];
578
+ }
579
+ }
580
+
581
+ // src/core/switch.ts
582
+ var DET_IF = /^(\s*)\$IF\s+([^:]*?)\s*:\s*(.*)$/i;
583
+ var ELSE_IF = /^(\s*)\$ELSE\s+IF\s+([^:]*?)\s*:\s*(.*)$/i;
584
+ var ELSE_A = /^(\s*)\$ELSE\s*:\s*(.*)$/i;
585
+ var PROSE_IF_LEAD = /^\s*IF\b[^:]*:\s*/i;
586
+ var LEGACY_DET_IF = /^(\s*)IF\s+([^:]*(?:\$|@\{)[a-z][a-z0-9_]*[^:]*?)\s*:\s*(.*)$/i;
587
+ var LEGACY_ELSE_IF = /^(\s*)ELSE\s+IF\s+([^:]*?)\s*:\s*(.*)$/i;
588
+ var LEGACY_ELSE = /^(\s*)ELSE\s*:\s*(.*)$/i;
589
+ var NEW_HEADER = /^(\s*)\$SWITCH\s+ON\s+@\{([a-z][a-z0-9_]*)\}\s*$/i;
590
+ var NEW_ARM = /^(\s*)-\s+([A-Za-z][A-Za-z0-9_]*)\s*::\s*(.*)$/;
591
+ var NEW_ARM_ANY = /^(\s*)-\s+(.*?)\s*::\s*(.*)$/;
592
+ var OLD_VALUE_HEADER = /^(\s*)\$SWITCH\s+ON\s+\$([a-z][a-z0-9_]*)\s*:\s*$/i;
593
+ var OLD_TUPLE_HEADER = /^(\s*)\$SWITCH\s+ON\s+(\$[a-z][a-z0-9_]*(?:\s*,\s*\$[a-z][a-z0-9_]*)+)\s*:\s*$/i;
594
+ var OLD_PRED_HEADER = /^(\s*)\$SWITCH\s*:\s*$/i;
595
+ var OLD_CASE_ARM = /^\s*CASE\b\s*(.*?)\s*:\s*(.*)$/i;
596
+ var OLD_ELSE_ARM = /^\s*ELSE\s*:\s*(.*)$/i;
597
+ var ANY_OLD_SWITCH = /^\s*\$SWITCH\b/i;
598
+ var leading = (s) => s.length - s.trimStart().length;
599
+ function andLeaves(e) {
600
+ return e.kind === "and" ? [...andLeaves(e.left), ...andLeaves(e.right)] : [e];
601
+ }
602
+ function classifyChain(arms, conds) {
603
+ const caseArms = arms.filter((a) => a.kind === "case");
604
+ const asPredicate = () => {
605
+ caseArms.forEach((a) => {
606
+ a.predicate = a.condition ?? "";
607
+ });
608
+ return { kind: "predicate", varName: "" };
609
+ };
610
+ if (conds.some((c) => c === null)) return asPredicate();
611
+ const valueOf = (e) => e.kind === "is" && !e.negate ? { name: e.name, values: [e.value] } : e.kind === "oneOf" ? { name: e.name, values: e.values } : null;
612
+ const vals = conds.map((c) => valueOf(c));
613
+ if (vals.every((v) => v !== null) && new Set(vals.map((v) => v.name)).size === 1) {
614
+ caseArms.forEach((a, i) => {
615
+ a.values = (vals[i]?.values ?? []).map((v) => v.toLowerCase());
616
+ });
617
+ return { kind: "value", varName: vals[0].name };
618
+ }
619
+ const rows = conds.map((c) => {
620
+ const leaves = andLeaves(c);
621
+ const row = /* @__PURE__ */ new Map();
622
+ for (const l of leaves) {
623
+ if (l.kind !== "is" || l.negate || row.has(l.name)) return null;
624
+ row.set(l.name, l.value.toLowerCase());
625
+ }
626
+ return row;
627
+ });
628
+ if (rows.every((r) => r !== null)) {
629
+ const order = [];
630
+ for (const r of rows) for (const name of r.keys()) if (!order.includes(name)) order.push(name);
631
+ if (order.length >= 2) {
632
+ caseArms.forEach((a, i) => {
633
+ a.patterns = order.map((name) => rows[i].get(name) ?? "_");
634
+ });
635
+ return { kind: "tuple", varName: "", varNames: order };
636
+ }
637
+ }
638
+ return asPredicate();
639
+ }
640
+ function composeBlockRewrite(lines, headerLine, blockEnd, varName) {
641
+ const pad = " ".repeat(leading(lines[headerLine] ?? ""));
642
+ const out = [`${pad}$SWITCH ON @{${varName}}`];
643
+ for (let k = headerLine + 1; k < blockEnd; k += 1) {
644
+ const raw = lines[k] ?? "";
645
+ if (raw.trim() === "") continue;
646
+ const em = raw.match(OLD_ELSE_ARM);
647
+ const cm = em ? null : raw.match(OLD_CASE_ARM);
648
+ if (em) {
649
+ out.push(`${pad} - otherwise::${(em[1] ?? "").trim() !== "" ? ` ${(em[1] ?? "").trim()}` : ""}`);
650
+ continue;
651
+ }
652
+ if (!cm) continue;
653
+ const members = (cm[1] ?? "").split(",").map((v) => v.trim()).filter((v) => v !== "");
654
+ if (members.length !== 1 || !/^[A-Za-z][A-Za-z0-9_]*$/.test(members[0] ?? "")) return null;
655
+ out.push(`${pad} - ${members[0]}::${(cm[2] ?? "").trim() !== "" ? ` ${(cm[2] ?? "").trim()}` : ""}`);
656
+ }
657
+ return out.join("\n");
658
+ }
659
+ function composeChainRewrite(lines, headerLine, blockEnd, kind, varName, varNames) {
660
+ const pad = " ".repeat(leading(lines[headerLine] ?? ""));
661
+ const out = [];
662
+ let first = true;
663
+ for (let k = headerLine + 1; k < blockEnd; k += 1) {
664
+ const raw = lines[k] ?? "";
665
+ if (raw.trim() === "") continue;
666
+ const em = raw.match(OLD_ELSE_ARM);
667
+ const cm = em ? null : raw.match(OLD_CASE_ARM);
668
+ if (em) {
669
+ out.push(`${pad}$ELSE:${(em[1] ?? "").trim() !== "" ? ` ${(em[1] ?? "").trim()}` : ""}`);
670
+ continue;
671
+ }
672
+ if (!cm) continue;
673
+ const label = (cm[1] ?? "").trim();
674
+ const body = (cm[2] ?? "").trim();
675
+ let cond;
676
+ if (kind === "value") {
677
+ const members = label.split(",").map((v) => v.trim()).filter((v) => v !== "");
678
+ cond = members.length > 1 ? `@{${varName}} is one of ${members.join(", ")}` : `@{${varName}} is equal to ${members[0] ?? ""}`;
679
+ } else if (kind === "tuple") {
680
+ const pats = label.split(",").map((v) => v.trim());
681
+ const parts = pats.map((p, i) => p === "_" || p === "" ? null : `@{${varNames[i] ?? ""}} is equal to ${p}`).filter((p) => p !== null);
682
+ if (parts.length === 0) {
683
+ out.push(`${pad}$ELSE:${body !== "" ? ` ${body}` : ""}`);
684
+ continue;
685
+ }
686
+ cond = parts.join(" and ");
687
+ } else {
688
+ cond = pointerRewrite(label);
689
+ }
690
+ out.push(`${pad}${first ? "$IF" : "$ELSE IF"} ${cond}:${body !== "" ? ` ${body}` : ""}`);
691
+ first = false;
692
+ }
693
+ return out.join("\n");
694
+ }
695
+ function parseSwitches(lines) {
696
+ const blocks = [];
697
+ const exMask = examplesMask(lines);
698
+ const cfgMask = configSectionMask(lines);
699
+ const toolMask = toolBlockMask(lines);
700
+ let inFence = false;
701
+ const skip = lines.map((raw, i2) => {
702
+ const t = raw.trim();
703
+ if (t.startsWith("```") || t.startsWith("~~~")) {
704
+ inFence = !inFence;
705
+ return true;
706
+ }
707
+ return inFence || exMask[i2] === true || cfgMask[i2] === true || toolMask[i2] === true;
708
+ });
709
+ let i = 0;
710
+ while (i < lines.length) {
711
+ if (skip[i]) {
712
+ i += 1;
713
+ continue;
714
+ }
715
+ const raw0 = lines[i] ?? "";
716
+ const nm = raw0.match(NEW_HEADER);
717
+ if (nm) {
718
+ const headerIndent = (nm[1] ?? "").length;
719
+ const varName = (nm[2] ?? "").toLowerCase();
720
+ const arms = [];
721
+ let error = null;
722
+ let errorCode;
723
+ let armIndent = -1;
724
+ let k = i + 1;
725
+ while (k < lines.length) {
726
+ const raw = lines[k] ?? "";
727
+ if (raw.trim() === "") {
728
+ k += 1;
729
+ continue;
730
+ }
731
+ if (skip[k]) break;
732
+ const lead = leading(raw);
733
+ if (lead <= headerIndent) break;
734
+ if (armIndent === -1) armIndent = lead;
735
+ if (lead > armIndent) {
736
+ k += 1;
737
+ continue;
738
+ }
739
+ const am = raw.match(NEW_ARM);
740
+ if (am) {
741
+ const key = am[2] ?? "";
742
+ const body = (am[3] ?? "").trim();
743
+ if (/^otherwise$/i.test(key)) {
744
+ arms.push({ kind: "else", values: [], headerLine: k, spanEnd: k + 1, inlineBody: body });
745
+ } else {
746
+ const member = key.toLowerCase();
747
+ arms.push({ kind: "case", values: [member], condition: `@{${varName}} is equal to ${member}`, headerLine: k, spanEnd: k + 1, inlineBody: body });
748
+ }
749
+ k += 1;
750
+ continue;
751
+ }
752
+ const pm = raw.match(NEW_ARM_ANY);
753
+ if (pm) {
754
+ const label = (pm[2] ?? "").trim();
755
+ const pad = " ".repeat(headerIndent);
756
+ error ??= `a $SWITCH arm is a MEMBER of @{${varName}}'s domain, never a condition \u2014 "- ${label}::" asks a question, and questions are a chain's job. Write it as a chain:
757
+ ${pad}$IF ${/@\{/.test(label) ? label : pointerRewrite(label)}: \u2026
758
+ ${pad}$ELSE: \u2026`;
759
+ errorCode ??= "switch-arm";
760
+ k += 1;
761
+ continue;
762
+ }
763
+ error ??= `every row under $SWITCH ON is an arm \u2014 "- member:: \u2026" (or "- otherwise:: \u2026"); an arm's body sits on the lines indented beneath its row.`;
764
+ errorCode ??= "switch-arm";
765
+ k += 1;
766
+ }
767
+ const blockEnd = k;
768
+ for (let a = 0; a < arms.length; a += 1) {
769
+ const arm = arms[a];
770
+ if (arm) arm.spanEnd = arms[a + 1]?.headerLine ?? blockEnd;
771
+ }
772
+ if (arms.length === 0) {
773
+ error ??= 'the $SWITCH ON block has no arms \u2014 add "- member:: \u2026" rows one unit beneath the header.';
774
+ errorCode ??= "switch-arm";
775
+ }
776
+ blocks.push({ kind: "value", form: "block", headerLine: i, blockEnd, indent: headerIndent, varName, arms, error, ...errorCode !== void 0 ? { errorCode } : {} });
777
+ i = blockEnd;
778
+ continue;
779
+ }
780
+ const om = raw0.match(OLD_VALUE_HEADER);
781
+ const otm = om ? null : raw0.match(OLD_TUPLE_HEADER);
782
+ const opm = om || otm ? null : raw0.match(OLD_PRED_HEADER);
783
+ if (om || otm || opm) {
784
+ const indent = ((om ?? otm ?? opm)?.[1] ?? "").length;
785
+ let j = i + 1;
786
+ while (j < lines.length) {
787
+ const l = lines[j] ?? "";
788
+ if (l.trim() !== "" && leading(l) <= indent) break;
789
+ j += 1;
790
+ }
791
+ const kind = om ? "value" : otm ? "tuple" : "predicate";
792
+ const varName = (om?.[2] ?? "").toLowerCase();
793
+ const varNames = otm ? (otm[2] ?? "").split(",").map((v) => v.trim().replace(/^\$/, "").toLowerCase()) : [];
794
+ const blockRewrite = om ? composeBlockRewrite(lines, i, j, varName) : null;
795
+ const rewrite = blockRewrite ?? composeChainRewrite(lines, i, j, kind, varName, varNames);
796
+ blocks.push({
797
+ kind: "predicate",
798
+ form: "block",
799
+ headerLine: i,
800
+ blockEnd: j,
801
+ indent,
802
+ varName: "",
803
+ arms: [],
804
+ error: blockRewrite !== null ? `the CASE form is retired \u2014 a $SWITCH block's arms are "- member::" rows under a "@{name}" header. Replace the block with:
805
+ ${rewrite}` : `this $SWITCH form is retired \u2014 conditions belong to a $IF chain (members-only blocks use "$SWITCH ON @{x}" with "- member::" rows). Replace the block with:
806
+ ${rewrite}`
807
+ });
808
+ i = j;
809
+ continue;
810
+ }
811
+ if (ANY_OLD_SWITCH.test(raw0)) {
812
+ blocks.push({ kind: "predicate", form: "block", headerLine: i, blockEnd: i + 1, indent: leading(raw0), varName: "", arms: [], error: `malformed $SWITCH \u2014 the block form is "$SWITCH ON @{x}" (a pointer, no colon) with "- member:: \u2026" arm rows one unit beneath; for conditions, write a $IF chain instead.` });
813
+ i += 1;
814
+ continue;
815
+ }
816
+ const m = raw0.match(DET_IF);
817
+ const lm = m ? null : raw0.match(LEGACY_DET_IF);
818
+ if (m || lm) {
819
+ const head = m ?? lm;
820
+ const indent = (head[1] ?? "").length;
821
+ const legacyArms = lm ? [i] : [];
822
+ const arms = [{ kind: "case", values: [], condition: (head[2] ?? "").trim(), headerLine: i, spanEnd: i + 1, inlineBody: (head[3] ?? "").trim() }];
823
+ let error = null;
824
+ let k = i + 1;
825
+ while (k < lines.length) {
826
+ const raw = lines[k] ?? "";
827
+ if (raw.trim() === "") {
828
+ k += 1;
829
+ continue;
830
+ }
831
+ if (skip[k]) break;
832
+ const lead = leading(raw);
833
+ if (lead > indent) {
834
+ if (DET_IF.test(raw) || LEGACY_DET_IF.test(raw)) error ??= "a nested deterministic $IF is not supported \u2014 combine the conditions with `and`, or lift the inner chain out.";
835
+ k += 1;
836
+ continue;
837
+ }
838
+ if (lead < indent) break;
839
+ const eim = raw.match(ELSE_IF);
840
+ const lei = eim ? null : raw.match(LEGACY_ELSE_IF);
841
+ const em = eim ?? lei ? null : raw.match(ELSE_A);
842
+ const lem = eim ?? lei ?? em ? null : raw.match(LEGACY_ELSE);
843
+ if (eim || lei) {
844
+ const am = eim ?? lei;
845
+ const cond = (am[2] ?? "").trim();
846
+ if (lei) legacyArms.push(k);
847
+ if (!/@\{[a-z]|\$[a-z]/i.test(cond)) error ??= `$ELSE IF binds to the deterministic plane \u2014 its condition must read a @{variable} pointer (found "${cond}"). For a prose alternative, write it as its own sentence.`;
848
+ arms.push({ kind: "case", values: [], condition: cond, headerLine: k, spanEnd: k + 1, inlineBody: (am[3] ?? "").trim() });
849
+ k += 1;
850
+ continue;
851
+ }
852
+ if (em || lem) {
853
+ const am = em ?? lem;
854
+ if (lem) legacyArms.push(k);
855
+ arms.push({ kind: "else", values: [], headerLine: k, spanEnd: k + 1, inlineBody: (am[2] ?? "").trim() });
856
+ k += 1;
857
+ continue;
858
+ }
859
+ break;
860
+ }
861
+ const blockEnd = k;
862
+ for (let a = 0; a < arms.length; a += 1) {
863
+ const arm = arms[a];
864
+ if (arm) arm.spanEnd = arms[a + 1]?.headerLine ?? blockEnd;
865
+ }
866
+ if (legacyArms.length > 0) {
867
+ const pad = " ".repeat(indent);
868
+ const rewritten = arms.map(
869
+ (a, ix) => a.kind === "else" ? `${pad}$ELSE:${a.inlineBody !== "" ? ` ${a.inlineBody}` : ""}` : `${pad}${ix === 0 ? "$IF" : "$ELSE IF"} ${pointerRewrite(a.condition ?? "")}:${a.inlineBody !== "" ? ` ${a.inlineBody}` : ""}`
870
+ ).join("\n");
871
+ error = `a deterministic chain leads with $ \u2014 the arms are commands ($IF / $ELSE IF / $ELSE), and a variable in a condition is a @{name} pointer. Replace the arm lines with:
872
+ ${rewritten}`;
873
+ }
874
+ const conds = arms.filter((a) => a.kind === "case").map((a) => parseExpr(a.condition ?? "").ast);
875
+ const cls = classifyChain(arms, conds);
876
+ blocks.push({ kind: cls.kind, form: "chain", headerLine: i, blockEnd, indent, varName: cls.varName, ...cls.varNames !== void 0 ? { varNames: cls.varNames } : {}, arms, error });
877
+ i = blockEnd;
878
+ continue;
879
+ }
880
+ const strayElseIf = raw0.match(ELSE_IF) ?? raw0.match(LEGACY_ELSE_IF);
881
+ const strayElse = strayElseIf ? null : raw0.match(ELSE_A) ?? raw0.match(LEGACY_ELSE);
882
+ if (strayElseIf || strayElse) {
883
+ let p = i - 1;
884
+ while (p >= 0 && ((lines[p] ?? "").trim() === "" || leading(lines[p] ?? "") > leading(raw0) || /^\s*(?:[-*+]|\d+[.)])\s/.test(lines[p] ?? ""))) p -= 1;
885
+ const prev = lines[p] ?? "";
886
+ const afterProseIf = PROSE_IF_LEAD.test(prev) && !DET_IF.test(prev) && !LEGACY_DET_IF.test(prev);
887
+ blocks.push({
888
+ kind: "predicate",
889
+ form: "chain",
890
+ headerLine: i,
891
+ blockEnd: i + 1,
892
+ indent: leading(raw0),
893
+ varName: "",
894
+ arms: [],
895
+ error: afterProseIf ? `$ELSE binds to a deterministic $IF \u2014 the IF above is prose, so the model weighs it. Write the alternative as prose (e.g. lead with OTHERWISE), or make the chain deterministic: lead it with $IF and read a @{variable} pointer in the condition.` : `$ELSE without a deterministic $IF above it at the same column \u2014 a chain reads $IF @{x} \u2026: / $ELSE IF \u2026: / $ELSE:.`
896
+ });
897
+ i += 1;
898
+ continue;
899
+ }
900
+ if (OLD_CASE_ARM.test(raw0) && /^\s*CASE\b/i.test(raw0)) {
901
+ blocks.push({ kind: "predicate", form: "block", headerLine: i, blockEnd: i + 1, indent: leading(raw0), varName: "", arms: [], error: `CASE is retired \u2014 a $SWITCH block's arms are "- member:: \u2026" rows (or write a $IF chain: "$IF @{x} is equal to <value>: \u2026").` });
902
+ i += 1;
903
+ continue;
904
+ }
905
+ i += 1;
906
+ }
907
+ return blocks;
908
+ }
909
+ function selectArm(block, value) {
910
+ const v = value.trim().toLowerCase();
911
+ return block.arms.find((a) => a.kind === "case" && a.values.includes(v)) ?? block.arms.find((a) => a.kind === "else") ?? null;
912
+ }
913
+ function selectPredicateArm(block, test) {
914
+ for (const a of block.arms) {
915
+ if (a.kind === "else") return a;
916
+ if (a.predicate !== void 0 && test(a.predicate)) return a;
917
+ }
918
+ return null;
919
+ }
920
+ function selectTupleArm(block, values) {
921
+ const v = values.map((x) => x.trim().toLowerCase());
922
+ const match = block.arms.find(
923
+ (a) => a.kind === "case" && a.patterns !== void 0 && a.patterns.length === v.length && a.patterns.every((p, i) => p === "_" || p === v[i])
924
+ );
925
+ return match ?? block.arms.find((a) => a.kind === "else") ?? null;
926
+ }
927
+ var INLINE_SWITCH = /\$SWITCH\s+ON\s+\$([a-z][a-z0-9_]*)\s*\(([^)]*)\)/gi;
928
+ function parseInlineCases(body) {
929
+ const cases = [];
930
+ let error = null;
931
+ for (const seg of body.split("|")) {
932
+ if (seg.trim() === "") continue;
933
+ const colon = seg.indexOf(":");
934
+ if (colon < 0) {
935
+ error ??= "each arm needs a `value: fragment`.";
936
+ continue;
937
+ }
938
+ const label = seg.slice(0, colon).trim();
939
+ const frag = seg.slice(colon + 1).trim();
940
+ if (label === "_" || /^else$/i.test(label)) {
941
+ cases.push({ values: [], isDefault: true, frag });
942
+ continue;
943
+ }
944
+ const values = label.split(",").map((v) => v.trim().toLowerCase()).filter((v) => v !== "");
945
+ if (values.length === 0) {
946
+ error ??= "an arm has no value before its colon.";
947
+ continue;
948
+ }
949
+ cases.push({ values, isDefault: false, frag });
950
+ }
951
+ if (cases.length === 0) error ??= "the inline $SWITCH has no arms.";
952
+ return { cases, error };
953
+ }
954
+ function parseInlineSwitches(lines) {
955
+ const out = [];
956
+ lines.forEach((line, i) => {
957
+ for (const m of line.matchAll(INLINE_SWITCH)) {
958
+ const { cases, error } = parseInlineCases(m[2] ?? "");
959
+ const start = m.index ?? 0;
960
+ out.push({ line: i, start, end: start + m[0].length, varName: (m[1] ?? "").toLowerCase(), cases, error });
961
+ }
962
+ });
963
+ return out;
964
+ }
965
+ function resolveInlineText(text, pick) {
966
+ return text.replace(INLINE_SWITCH, (whole, varName, body) => {
967
+ const { cases, error } = parseInlineCases(body);
968
+ if (error !== null) return whole;
969
+ const v = pick(String(varName).toLowerCase()).trim().toLowerCase();
970
+ const arm = cases.find((c) => !c.isDefault && c.values.includes(v)) ?? cases.find((c) => c.isDefault) ?? null;
971
+ return arm ? arm.frag : "";
972
+ });
973
+ }
974
+ function maskInlineSwitches(source) {
975
+ return source.replace(INLINE_SWITCH, (m) => " ".repeat(m.length));
976
+ }
977
+ function joinOr(values) {
978
+ if (values.length <= 1) return values[0] ?? "";
979
+ return `${values.slice(0, -1).join(", ")} or ${values[values.length - 1]}`;
980
+ }
981
+ function conditionPhrase(_block, arm) {
982
+ if (arm.kind === "else") return "Otherwise";
983
+ const smoothed = (arm.condition ?? arm.predicate ?? "").replace(/@\{([a-z][a-z0-9_]*)\}/gi, "$1").replace(/\$/g, "").trim().replace(/\bis\s+not\s+equal\s+to\s+/gi, "is not ").replace(/\bis\s+equal\s+to\s+/gi, "is ").replace(/\bis\s+one\s+of\s+((?:[a-z0-9_-]+\s*,\s*)*[a-z0-9_-]+)/gi, (_m, list) => `is ${joinOr(list.split(",").map((v) => v.trim()).filter((v) => v !== ""))}`);
984
+ return `If ${smoothed}`;
985
+ }
986
+ function switchBlockMask(lines) {
987
+ const mask = new Array(lines.length).fill(false);
988
+ for (const block of parseSwitches(lines)) {
989
+ if (block.error !== null) continue;
990
+ for (let l = block.headerLine; l < block.blockEnd && l < mask.length; l += 1) mask[l] = true;
991
+ }
992
+ return mask;
993
+ }
994
+ function switchProseRewrite(lines) {
995
+ const rw = /* @__PURE__ */ new Map();
996
+ for (const block of parseSwitches(lines)) {
997
+ if (block.error !== null) continue;
998
+ const pad = " ".repeat(block.indent);
999
+ for (let l = block.headerLine; l < block.blockEnd; l += 1) rw.set(l, null);
1000
+ for (const arm of block.arms) {
1001
+ const cond = conditionPhrase(block, arm);
1002
+ rw.set(arm.headerLine, arm.inlineBody !== "" ? `${pad}${cond}: ${arm.inlineBody}` : `${pad}${cond}:`);
1003
+ const bodyLines = [];
1004
+ for (let l = arm.headerLine + 1; l < arm.spanEnd; l += 1) bodyLines.push(l);
1005
+ const nonBlank = bodyLines.filter((l) => (lines[l] ?? "").trim() !== "");
1006
+ if (nonBlank.length === 0) continue;
1007
+ const base = Math.min(...nonBlank.map((l) => leading(lines[l] ?? "")));
1008
+ for (const l of bodyLines) {
1009
+ const raw = lines[l] ?? "";
1010
+ if (raw.trim() === "") {
1011
+ rw.set(l, "");
1012
+ continue;
1013
+ }
1014
+ rw.set(l, " ".repeat(block.indent + 2 + Math.max(0, leading(raw) - base)) + raw.trimStart());
1015
+ }
1016
+ }
1017
+ }
1018
+ return rw;
1019
+ }
1020
+ function expandInlineToProse(text) {
1021
+ return text.replace(INLINE_SWITCH, (whole, _v, body) => {
1022
+ const { cases, error } = parseInlineCases(body);
1023
+ if (error !== null) return whole;
1024
+ const arm = cases.find((c) => c.isDefault) ?? cases[0];
1025
+ return arm ? arm.frag : "";
1026
+ });
1027
+ }
1028
+
1029
+ // src/core/type-system.ts
1030
+ var PRIM_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean"]);
1031
+ var PLURAL_PRIMS = { strings: "string", numbers: "number", integers: "integer", booleans: "boolean" };
1032
+ var PRIM_SYNONYMS = { bool: "boolean", int: "integer", text: "string" };
1033
+ var MEMBER_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
1034
+ var QUANT_RE = /^(one|any)\s+of\s+(.+)$/i;
1035
+ var LIST_RE = /^list\s+of\s+(\S+)$/i;
1036
+ function parseMembers(quant, tail) {
1037
+ const members = tail.split(",").map((m) => m.trim()).filter((m) => m !== "");
1038
+ if (members.length < 2) return { reason: `"${quant} of" declares a closed set \u2014 it needs at least two comma-separated members.` };
1039
+ const bad = members.find((m) => !MEMBER_RE.test(m));
1040
+ if (bad !== void 0) return { reason: `"${bad}" is not a valid set member \u2014 members are bare names (letters, digits, _ or -); the description sits after a ":".` };
1041
+ const dup = members.find((m, i) => members.findIndex((x) => x.toLowerCase() === m.toLowerCase()) !== i);
1042
+ if (dup !== void 0) return { reason: `"${dup}" is listed twice in the ${quant} of set.` };
1043
+ return { members };
1044
+ }
1045
+ function typeIntended(text) {
1046
+ const t = text.trim();
1047
+ if (QUANT_RE.test(t) || /^list\s+of\b/i.test(t) || /^optional\b/i.test(t)) return true;
1048
+ const first = (t.split(/[\s,]/, 1)[0] ?? "").toLowerCase();
1049
+ if (PRIM_TYPES.has(first) || first in PRIM_SYNONYMS) return true;
1050
+ const segs = t.split(",").map((s) => s.trim());
1051
+ return segs.length >= 2 && segs.every((s) => MEMBER_RE.test(s));
1052
+ }
1053
+ var REFINEMENT_KEYWORDS = ["BETWEEN", "AT LEAST", "AT MOST", "UP TO", "MATCHING"];
1054
+ var REFINE_HEAD = new RegExp(`^(${REFINEMENT_KEYWORDS.map((k) => k.toLowerCase().replace(/ /g, "\\s+")).join("|")})\\b`, "i");
1055
+ var NUM = String.raw`(-?\d+(?:\.\d+)?)`;
1056
+ function parseRefinement(prim, tail) {
1057
+ const raw = tail.trim();
1058
+ if (!REFINE_HEAD.test(raw)) return null;
1059
+ if (prim === "boolean") return { reason: `boolean takes no constraint \u2014 drop "${raw}".` };
1060
+ const mm = raw.match(/^matching\s+\/(.+)\/$/i);
1061
+ if (mm) {
1062
+ if (prim !== "string") return { reason: `"matching" applies to string, not ${prim}.` };
1063
+ try {
1064
+ new RegExp(mm[1]);
1065
+ } catch {
1066
+ return { reason: `"/${mm[1]}/" is not a valid regular expression.` };
1067
+ }
1068
+ return { refine: { pattern: mm[1] } };
1069
+ }
1070
+ if (/^matching\b/i.test(raw)) return { reason: `"matching" takes a /regex/ \u2014 e.g. matching /^[a-z]+$/.` };
1071
+ const isLen = /\bcharacters?\b/i.test(raw);
1072
+ if (isLen && prim !== "string") return { reason: `a character-length constraint applies to string, not ${prim}.` };
1073
+ if (!isLen && prim === "string") return { reason: `a numeric range applies to number|integer; for string, add "characters" or use a pattern.` };
1074
+ const body = raw.replace(/\s+characters?\s*$/i, "").trim();
1075
+ let m;
1076
+ let bounds = null;
1077
+ if (m = body.match(new RegExp(`^between\\s+${NUM}\\s+and\\s+${NUM}$`, "i"))) {
1078
+ const lo = Number(m[1]);
1079
+ const hi = Number(m[2]);
1080
+ if (lo > hi) return { reason: `the lower bound (${lo}) is above the upper (${hi}).` };
1081
+ bounds = { min: lo, max: hi };
1082
+ } else if (m = body.match(new RegExp(`^at\\s+least\\s+${NUM}$`, "i"))) bounds = { min: Number(m[1]) };
1083
+ else if (m = body.match(new RegExp(`^at\\s+most\\s+${NUM}$`, "i"))) bounds = { max: Number(m[1]) };
1084
+ else if (m = body.match(new RegExp(`^up\\s+to\\s+${NUM}$`, "i"))) bounds = { max: Number(m[1]) };
1085
+ if (!bounds) return { reason: `"${raw}" isn't a recognized ${prim} constraint \u2014 try "between A and B", "at least N", "at most N"${prim === "string" ? ', "up to N characters"' : ""}.` };
1086
+ const key = isLen ? ["minLen", "maxLen"] : ["min", "max"];
1087
+ const refine = {};
1088
+ if (bounds.min !== void 0) refine[key[0]] = bounds.min;
1089
+ if (bounds.max !== void 0) refine[key[1]] = bounds.max;
1090
+ return { refine };
1091
+ }
1092
+ function parseTypeExpr(text) {
1093
+ const t = text.trim();
1094
+ if (t === "") return null;
1095
+ const q = t.match(QUANT_RE);
1096
+ if (q) {
1097
+ const quant = (q[1] ?? "one").toLowerCase();
1098
+ const r = parseMembers(quant, q[2] ?? "");
1099
+ if ("reason" in r) return { ok: false, ...r };
1100
+ return { ok: true, expr: quant === "one" ? { kind: "oneOf", members: r.members } : { kind: "anyOf", members: r.members } };
1101
+ }
1102
+ const l = t.match(LIST_RE);
1103
+ if (l) {
1104
+ const unit = (l[1] ?? "").toLowerCase();
1105
+ const prim = PLURAL_PRIMS[unit];
1106
+ if (prim) return { ok: true, expr: { kind: "list", prim } };
1107
+ if (PRIM_TYPES.has(unit)) return { ok: false, reason: `"list of" takes a plural base type.`, rewrite: `list of ${unit}s` };
1108
+ return { ok: false, reason: `"list of ${l[1] ?? ""}" is not a type \u2014 the base types are strings, numbers, integers, booleans.` };
1109
+ }
1110
+ if (/^list\s+of\b/i.test(t)) return { ok: false, reason: `"list of" takes exactly one plural base type: strings, numbers, integers, or booleans.` };
1111
+ const low = t.toLowerCase();
1112
+ const primHead = t.match(/^(string|number|integer|boolean)\b\s*(.*)$/i);
1113
+ if (primHead) {
1114
+ const prim = primHead[1].toLowerCase();
1115
+ const rest = (primHead[2] ?? "").trim();
1116
+ if (rest === "") return { ok: true, expr: { kind: "prim", prim } };
1117
+ const ref = parseRefinement(prim, rest);
1118
+ if (ref === null) return null;
1119
+ if ("reason" in ref) return { ok: false, reason: ref.reason };
1120
+ return { ok: true, expr: { kind: "prim", prim, refine: ref.refine } };
1121
+ }
1122
+ const syn = PRIM_SYNONYMS[low];
1123
+ if (syn) return { ok: false, reason: `"${t}" is not a canonical type \u2014 TypeGlish has one spelling per type.`, rewrite: syn };
1124
+ const segs = t.split(",").map((s) => s.trim());
1125
+ if (segs.length >= 2 && segs.every((s) => MEMBER_RE.test(s))) {
1126
+ return {
1127
+ ok: false,
1128
+ reason: `"${t}" doesn't say whether to pick one or many \u2014 write "one of ${segs.join(", ")}" (exactly one) or "any of ${segs.join(", ")}" (one or more).`,
1129
+ rewrite: `one of ${segs.join(", ")}`
1130
+ };
1131
+ }
1132
+ return null;
1133
+ }
1134
+ function translateLegacyTypeHead(head) {
1135
+ const t = head.trim();
1136
+ const arr = t.match(/^(string|number|integer|boolean)\[\]$/);
1137
+ if (arr) return `list of ${arr[1]}s`;
1138
+ const dflt = t.match(/^(string|number|integer|boolean)\s*=\s*(\S.*?)$/);
1139
+ if (dflt) return `${dflt[1]} default to ${(dflt[2] ?? "").trim()}`;
1140
+ if (t.includes("|")) {
1141
+ const members = t.split("|").map((m) => m.trim()).filter((m) => m !== "");
1142
+ if (members.length >= 2) return `one of ${members.join(", ")}`;
1143
+ }
1144
+ const syn = PRIM_SYNONYMS[t.toLowerCase()];
1145
+ if (syn) return syn;
1146
+ return null;
1147
+ }
1148
+ function renderRefine(r) {
1149
+ if (r.pattern !== void 0) return `matching /${r.pattern}/`;
1150
+ if (r.minLen !== void 0 && r.maxLen !== void 0) return `between ${r.minLen} and ${r.maxLen} characters`;
1151
+ if (r.minLen !== void 0) return `at least ${r.minLen} characters`;
1152
+ if (r.maxLen !== void 0) return `up to ${r.maxLen} characters`;
1153
+ if (r.min !== void 0 && r.max !== void 0) return `between ${r.min} and ${r.max}`;
1154
+ if (r.min !== void 0) return `at least ${r.min}`;
1155
+ if (r.max !== void 0) return `at most ${r.max}`;
1156
+ return "";
1157
+ }
1158
+ function renderTypeExpr(e) {
1159
+ switch (e.kind) {
1160
+ case "prim":
1161
+ return e.refine ? `${e.prim} ${renderRefine(e.refine)}` : e.prim;
1162
+ case "list":
1163
+ return `list of ${e.prim}s`;
1164
+ case "oneOf":
1165
+ return `one of ${e.members.join(", ")}`;
1166
+ case "anyOf":
1167
+ return `any of ${e.members.join(", ")}`;
1168
+ }
1169
+ }
1170
+
1171
+ // src/core/tool.ts
1172
+ var TOOL_KEYS = /* @__PURE__ */ new Set(["description", "input", "request", "headers"]);
1173
+ var SERVICE_KEYS = /* @__PURE__ */ new Set(["base", "headers"]);
1174
+ var FIELD_RE = /^(\s+)-\s+([a-z][a-z0-9_-]*)::\s*(.*)$/;
1175
+ var PARAM_RE = /^-\s+([a-z][a-z0-9_]*)(\?)?::\s*(.*)$/;
1176
+ var HEADER_RE = /^-\s+([A-Za-z0-9-]+)::\s*(\S.*?)\s*$/;
1177
+ var LEGACY_FIELD_RE = /^\s*(?:[-*+]\s+)?([A-Za-z][\w-]*)(\??):(?!:)\s*(.*)$/;
1178
+ var BARE_FIELD_RE = /^\s*([A-Za-z][\w-]*)(\??)::\s*(.*)$/;
1179
+ var BINDING_ABS_RE = /^(GET|POST|PUT|PATCH|DELETE)\s+(https?:\/\/\S+)$/;
1180
+ var BINDING_SVC_RE = /^(GET|POST|PUT|PATCH|DELETE)\s+([a-z][a-z0-9_]*)\s+(\/\S*)$/;
1181
+ var OPTIONAL_LEAD_RE = /^optional\b\s*/i;
1182
+ var DEFAULT_TAIL_RE = /\s+default\s+to\s+(\S.*)$/i;
1183
+ var BAD_DEFAULT_TAIL_RE = /\s+(?:=\s*(\S.*)|defaults\s+to\s+(\S.*)|default\s+(?!to\b)(\S.*))$/i;
1184
+ var PLACEHOLDER_RE = /@\{([A-Za-z][A-Za-z0-9_.]*)\}/g;
1185
+ var LEGACY_PLACEHOLDER_RE = /(?<!@)\{([A-Za-z][A-Za-z0-9_.]*)\}/g;
1186
+ var SECRET_RE = /\b(?:sk-[A-Za-z0-9_-]{10,}|ghp_[A-Za-z0-9]{20,}|xoxb-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16})\b|^Bearer\s+[A-Za-z0-9._~+/=-]{20,}$/;
1187
+ function scanBlocks(rawLines, decl) {
1188
+ const blocks = [];
1189
+ let inFence = false;
1190
+ for (let i = 0; i < rawLines.length; i += 1) {
1191
+ const trimmed = (rawLines[i] ?? "").trim();
1192
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
1193
+ inFence = !inFence;
1194
+ continue;
1195
+ }
1196
+ const m = inFence ? null : (rawLines[i] ?? "").match(decl);
1197
+ if (!m) continue;
1198
+ const headerIndent = ((rawLines[i] ?? "").match(/^[ \t]*/) ?? [""])[0].length;
1199
+ const body = [];
1200
+ for (let j = i + 1; j < rawLines.length; j += 1) {
1201
+ const line = rawLines[j] ?? "";
1202
+ if (line.trim() === "") continue;
1203
+ if ((line.match(/^[ \t]*/) ?? [""])[0].length <= headerIndent) break;
1204
+ body.push({ raw: line, line: j });
1205
+ }
1206
+ blocks.push({ name: (m[1] ?? "").toLowerCase(), line: i, body });
1207
+ if (body.length > 0) i = body[body.length - 1]?.line ?? i;
1208
+ }
1209
+ return blocks;
1210
+ }
1211
+ function rewriteRow(key, opt, value) {
1212
+ const translated = translateLegacyValue(value.trim()) ?? value.trim();
1213
+ const v = opt === "?" && translated !== "" ? `optional ${translated}` : opt === "?" ? "optional" : translated;
1214
+ const row = `- ${key}:: ${v}`.trimEnd();
1215
+ return /[.!?:]["')\]]*$/.test(row) ? row : `${row}.`;
1216
+ }
1217
+ function translateLegacyValue(value) {
1218
+ const comma = value.indexOf(",");
1219
+ const head = stripPunct(comma >= 0 ? value.slice(0, comma) : value);
1220
+ const desc = comma >= 0 ? value.slice(comma + 1).trim() : "";
1221
+ let newHead = translateLegacyTypeHead(head);
1222
+ if (newHead === null && desc !== "" && parseTypeExpr(head)?.ok) newHead = head;
1223
+ if (newHead === null) return null;
1224
+ return desc !== "" ? `${newHead}: ${desc}` : newHead;
1225
+ }
1226
+ function composeValue(optional, head, dflt, desc) {
1227
+ const type = `${optional ? "optional " : ""}${head}${dflt !== null && dflt !== "" ? ` default to ${dflt}` : ""}`.trim();
1228
+ return desc !== "" ? `${type}: ${desc}` : type;
1229
+ }
1230
+ function buildParam(expr, dfltRaw, desc) {
1231
+ const base = expr.kind === "prim" ? { type: expr.prim } : expr.kind === "list" ? { type: expr.prim, array: true } : expr.kind === "oneOf" ? { type: "string", enum: expr.members } : { type: "string", array: true, enum: expr.members };
1232
+ if (dfltRaw !== null) {
1233
+ if (expr.kind === "list" || expr.kind === "anyOf") return { reason: `"default to" doesn't apply to a list \u2014 a default is one value.` };
1234
+ const raw = dfltRaw.trim().replace(/^["']|["']$/g, "");
1235
+ if (expr.kind === "prim" && (expr.prim === "number" || expr.prim === "integer")) {
1236
+ const n = Number(raw);
1237
+ if (Number.isNaN(n)) return { reason: `"default to ${raw}" isn't a number \u2014 if a description follows the default, it sits after ":", never a comma.` };
1238
+ base.default = n;
1239
+ } else if (expr.kind === "prim" && expr.prim === "boolean") {
1240
+ if (raw !== "true" && raw !== "false") return { reason: `"default to ${raw}" isn't a boolean \u2014 write true or false.` };
1241
+ base.default = raw === "true";
1242
+ } else if (expr.kind === "oneOf") {
1243
+ if (!expr.members.includes(raw)) return { reason: `"default to ${raw}" isn't a member of the set (${expr.members.join(", ")}).` };
1244
+ base.default = raw;
1245
+ } else {
1246
+ base.default = raw;
1247
+ }
1248
+ }
1249
+ if (desc !== "") base.description = desc;
1250
+ return base;
1251
+ }
1252
+ function shapedChildren(children, proseSafe) {
1253
+ const out = [];
1254
+ for (const c of children) {
1255
+ const bare = c.text.match(BARE_FIELD_RE);
1256
+ const legacy = c.text.match(LEGACY_FIELD_RE);
1257
+ if (bare) out.push({ line: c.line, rewrite: rewriteRow(bare[1] ?? "", bare[2] ?? "", bare[3] ?? "") });
1258
+ else if (legacy && (!proseSafe || legacy[2] === "?")) out.push({ line: c.line, rewrite: rewriteRow(legacy[1] ?? "", legacy[2] ?? "", legacy[3] ?? "") });
1259
+ }
1260
+ return out;
1261
+ }
1262
+ function scanFields(block) {
1263
+ const bodyIndent = (block.body[0]?.raw.match(/^[ \t]+/)?.[0] ?? "").length;
1264
+ const fields = [];
1265
+ for (const b of block.body) {
1266
+ if (b.raw.trim().startsWith("//")) continue;
1267
+ const indent = (b.raw.match(/^[ \t]+/)?.[0] ?? "").length;
1268
+ if (indent === bodyIndent) {
1269
+ const f = b.raw.match(FIELD_RE);
1270
+ if (f) fields.push({ key: (f[2] ?? "").toLowerCase(), value: (f[3] ?? "").trim(), line: b.line, children: [] });
1271
+ else fields.push({ key: "", value: b.raw.trim(), line: b.line, children: [] });
1272
+ } else if (fields.length > 0) {
1273
+ fields[fields.length - 1]?.children.push({ text: b.raw.trim(), line: b.line });
1274
+ } else {
1275
+ fields.push({ key: "", value: b.raw.trim(), line: b.line, children: [] });
1276
+ }
1277
+ }
1278
+ return fields;
1279
+ }
1280
+ function parseParamValue(value) {
1281
+ const v = value.trim();
1282
+ if (v === "") return { ok: true, param: { type: "string" }, optional: false };
1283
+ const legacy = translateLegacyValue(v);
1284
+ if (legacy !== null) {
1285
+ const reason = v.includes("|") ? `Closed sets are English \u2014 "one of a, b" (pick one) or "any of a, b" (pick many) \u2014 never "|" pipes.` : /\[\]/.test(v) ? `Arrays are English \u2014 "list of strings" (numbers, integers, booleans) \u2014 never "[]".` : /=/.test(v) ? `Defaults are English \u2014 "default to <value>" \u2014 never "=".` : /^(bool|int|text)\b/i.test(v) ? `TypeGlish has one spelling per type \u2014 string, number, integer, boolean.` : `The type and the description are separated by ":" \u2014 commas belong to lists and prose.`;
1286
+ return { ok: false, reason, value: legacy };
1287
+ }
1288
+ const ci = v.indexOf(":");
1289
+ const rawHead = ci >= 0 ? v.slice(0, ci) : v;
1290
+ const desc = ci >= 0 ? v.slice(ci + 1).trim() : "";
1291
+ let head = ci >= 0 ? rawHead.trim() : stripPunct(rawHead);
1292
+ const optional = OPTIONAL_LEAD_RE.test(head);
1293
+ if (optional) head = head.replace(OPTIONAL_LEAD_RE, "").trim();
1294
+ let dflt = null;
1295
+ const dm = head.match(DEFAULT_TAIL_RE);
1296
+ if (dm) {
1297
+ dflt = (dm[1] ?? "").trim();
1298
+ head = head.slice(0, dm.index).trim();
1299
+ } else {
1300
+ const bad = head.match(BAD_DEFAULT_TAIL_RE);
1301
+ if (bad) {
1302
+ const dv = (bad[1] ?? bad[2] ?? bad[3] ?? "").trim();
1303
+ return { ok: false, reason: `Defaults are written "default to ${dv}" \u2014 one canonical form, like "one of" and "at least".`, value: composeValue(optional, head.slice(0, bad.index).trim(), dv, desc) };
1304
+ }
1305
+ }
1306
+ const parsed = head === "" ? null : parseTypeExpr(head);
1307
+ if (parsed?.ok) {
1308
+ const built = buildParam(parsed.expr, dflt, desc);
1309
+ if ("reason" in built) return { ok: false, reason: built.reason };
1310
+ return { ok: true, param: built, optional };
1311
+ }
1312
+ if (parsed && !parsed.ok) {
1313
+ return { ok: false, reason: parsed.reason, ...parsed.rewrite !== void 0 ? { value: composeValue(optional, parsed.rewrite, dflt, desc) } : {} };
1314
+ }
1315
+ if (head === "" && (optional || dflt !== null)) {
1316
+ const built = buildParam({ kind: "prim", prim: "string" }, dflt, desc);
1317
+ if ("reason" in built) return { ok: false, reason: built.reason };
1318
+ return { ok: true, param: built, optional: true };
1319
+ }
1320
+ if (optional && head !== "" || dflt !== null || typeIntended(head)) {
1321
+ return {
1322
+ ok: false,
1323
+ reason: `"${head}" is not a type expression. The forms: string, number, integer, boolean, "list of strings", "one of a, b", "any of a, b", "optional <type>", "<type> default to <value>". If this is prose, reword it so it doesn't open with a type keyword.`
1324
+ };
1325
+ }
1326
+ return { ok: true, param: { type: "string", description: v }, optional: false };
1327
+ }
1328
+ function checkPlaceholders(text, line, allowedParams, where, flag) {
1329
+ PLACEHOLDER_RE.lastIndex = 0;
1330
+ let m;
1331
+ while ((m = PLACEHOLDER_RE.exec(text)) !== null) {
1332
+ const key = (m[1] ?? "").toLowerCase();
1333
+ if (key.startsWith("env.")) continue;
1334
+ if (allowedParams?.has(key)) continue;
1335
+ flag(
1336
+ line,
1337
+ allowedParams === null ? `@{${m[1]}} in ${where} \u2014 a $SERVICE is shared by many tools, so its values may only use @{env.NAME}.` : `@{${m[1]}} in ${where} isn't a declared parameter \u2014 declare it under input:, or use @{env.NAME} for environment values.`,
1338
+ "dangling-binding-param",
1339
+ "error"
1340
+ );
1341
+ }
1342
+ LEGACY_PLACEHOLDER_RE.lastIndex = 0;
1343
+ while ((m = LEGACY_PLACEHOLDER_RE.exec(text)) !== null) {
1344
+ flag(
1345
+ line,
1346
+ `A binding placeholder is a pointer \u2014 \`@{${m[1]}}\`, never \`{${m[1]}}\`. Rewrite the value as: "${text.replace(LEGACY_PLACEHOLDER_RE, "@{$1}").trim()}".`,
1347
+ "malformed-binding",
1348
+ "error"
1349
+ );
1350
+ break;
1351
+ }
1352
+ }
1353
+ function parseServices(rawLines) {
1354
+ const services = [];
1355
+ const conflicts = [];
1356
+ const flag = (line, message, code, severity) => {
1357
+ conflicts.push({ lines: [line], message, code, severity });
1358
+ };
1359
+ for (const block of scanBlocks(rawLines, SERVICE_DECL_RE)) {
1360
+ if (block.body.length === 0) {
1361
+ flag(block.line, `$SERVICE ${block.name} has no body \u2014 indent its fields (base::, headers::) beneath it.`, "empty-service", "error");
1362
+ continue;
1363
+ }
1364
+ const seen = /* @__PURE__ */ new Set();
1365
+ let base = "";
1366
+ const headers = [];
1367
+ for (const f of scanFields(block)) {
1368
+ if (f.key === "") {
1369
+ {
1370
+ const legacy = f.value.match(LEGACY_FIELD_RE);
1371
+ const bare = f.value.match(BARE_FIELD_RE);
1372
+ flag(
1373
+ f.line,
1374
+ legacy ? `Tool fields use the definition marker "::" with a leading bullet, never a single colon. Replace this line with: "${rewriteRow(legacy[1] ?? "", legacy[2] ?? "", legacy[3] ?? "")}".` : bare ? `Every tool field is a bullet row (the bullets are the compiled schema's nesting). Replace this line with: "${rewriteRow(bare[1] ?? "", bare[2] ?? "", bare[3] ?? "")}".` : `This line isn't a field of $SERVICE ${block.name}. A field is "- key:: value" (keys: base, headers). Found: "${f.value.slice(0, 60)}".`,
1375
+ "malformed-service",
1376
+ "error"
1377
+ );
1378
+ for (const kid of shapedChildren(f.children, false)) {
1379
+ flag(kid.line, `Unreachable \u2014 it sits under the malformed field above. After fixing that line, write this one as: "${kid.rewrite}".`, "malformed-service", "error");
1380
+ }
1381
+ }
1382
+ continue;
1383
+ }
1384
+ if (seen.has(f.key)) {
1385
+ flag(f.line, `Duplicate "${f.key}::" in $SERVICE ${block.name} \u2014 one per service.`, "duplicate-key", "error");
1386
+ continue;
1387
+ }
1388
+ seen.add(f.key);
1389
+ if (!SERVICE_KEYS.has(f.key)) {
1390
+ flag(f.line, `Unknown key "${f.key}::" in $SERVICE ${block.name} \u2014 known keys: base, headers.`, "unknown-service-key", "warn");
1391
+ } else if (f.key === "base") {
1392
+ const baseValue = stripPunct(f.value);
1393
+ if (/^https?:\/\/\S+$/.test(baseValue)) {
1394
+ base = baseValue.replace(/\/+$/, "");
1395
+ checkPlaceholders(base, f.line, null, `$SERVICE ${block.name}'s base`, flag);
1396
+ } else {
1397
+ flag(f.line, `Malformed base in $SERVICE ${block.name} \u2014 the value must be an absolute URL. Replace this line with the form: "- base:: https://api.example.com".`, "malformed-service", "error");
1398
+ }
1399
+ } else {
1400
+ for (const c of f.children) {
1401
+ const hm = c.text.match(HEADER_RE);
1402
+ if (!hm) {
1403
+ flag(c.line, `This isn't a header. A header is "- Header-Name:: value". Found: "${c.text.slice(0, 60)}".`, "malformed-header", "error");
1404
+ continue;
1405
+ }
1406
+ headers.push({ name: hm[1] ?? "", value: stripPunct(hm[2] ?? ""), line: c.line });
1407
+ checkPlaceholders(hm[2] ?? "", c.line, null, `$SERVICE ${block.name}'s headers`, flag);
1408
+ if (!/\{env\./.test(hm[2] ?? "") && SECRET_RE.test(hm[2] ?? "")) {
1409
+ flag(c.line, `This looks like a credential literal \u2014 never put secrets in the file; use @{env.NAME} instead.`, "secret-literal", "warn");
1410
+ }
1411
+ }
1412
+ }
1413
+ }
1414
+ if (base === "") {
1415
+ flag(block.line, `$SERVICE ${block.name} has no base \u2014 add a base field beneath it, like: "- base:: https://api.example.com".`, "service-no-base", "error");
1416
+ continue;
1417
+ }
1418
+ services.push({ name: block.name, base, headers: headers.map((h) => ({ name: h.name, value: h.value })), line: block.line });
1419
+ }
1420
+ const byName = /* @__PURE__ */ new Map();
1421
+ for (const s of services) {
1422
+ const list = byName.get(s.name) ?? [];
1423
+ list.push(s);
1424
+ byName.set(s.name, list);
1425
+ }
1426
+ for (const [dup, list] of byName) {
1427
+ if (list.length > 1) conflicts.push({ lines: list.map((s) => s.line), message: `$SERVICE ${dup} is defined ${list.length} times.`, code: "duplicate-service", severity: "error" });
1428
+ }
1429
+ return { services, conflicts };
1430
+ }
1431
+ function parseTools(rawLines) {
1432
+ const svc = parseServices(rawLines);
1433
+ const services = new Map(svc.services.map((s) => [s.name, s]));
1434
+ const usedServices = /* @__PURE__ */ new Set();
1435
+ const tools = [];
1436
+ const conflicts = [...svc.conflicts];
1437
+ const flag = (line, message, code, severity) => {
1438
+ conflicts.push({ lines: [line], message, code, severity });
1439
+ };
1440
+ for (const block of scanBlocks(rawLines, TOOL_DECL_RE)) {
1441
+ const name = block.name;
1442
+ if (block.body.length === 0) {
1443
+ flag(block.line, `$TOOL ${name} has no body \u2014 indent its fields (description::, input::, request::) beneath it.`, "empty-tool", "error");
1444
+ tools.push({ name, description: "", params: [], line: block.line });
1445
+ continue;
1446
+ }
1447
+ const seen = /* @__PURE__ */ new Set();
1448
+ const descParts = [];
1449
+ const params = [];
1450
+ let binding;
1451
+ const headers = [];
1452
+ for (const f of scanFields(block)) {
1453
+ if (f.key === "") {
1454
+ {
1455
+ const legacy = f.value.match(LEGACY_FIELD_RE);
1456
+ const bare = f.value.match(BARE_FIELD_RE);
1457
+ flag(
1458
+ f.line,
1459
+ legacy ? `Tool fields use the definition marker "::" with a leading bullet, never a single colon. Replace this line with: "${rewriteRow(legacy[1] ?? "", legacy[2] ?? "", legacy[3] ?? "")}".` : bare ? `Every tool field is a bullet row (the bullets are the compiled schema's nesting). Replace this line with: "${rewriteRow(bare[1] ?? "", bare[2] ?? "", bare[3] ?? "")}".` : `This line isn't a field of $TOOL ${name}. A field is "- key:: value" (keys: description, input, request, headers). Found: "${f.value.slice(0, 60)}".`,
1460
+ "malformed-tool",
1461
+ "error"
1462
+ );
1463
+ for (const kid of shapedChildren(f.children, false)) {
1464
+ flag(kid.line, `Unreachable \u2014 it sits under the malformed field above. After fixing that line, write this one as: "${kid.rewrite}".`, "malformed-param", "error");
1465
+ }
1466
+ }
1467
+ continue;
1468
+ }
1469
+ if (seen.has(f.key)) {
1470
+ flag(f.line, `Duplicate "${f.key}::" in $TOOL ${name} \u2014 one per tool.`, "duplicate-key", "error");
1471
+ continue;
1472
+ }
1473
+ seen.add(f.key);
1474
+ if (!TOOL_KEYS.has(f.key)) {
1475
+ flag(f.line, `Unknown key "${f.key}::" in $TOOL ${name} \u2014 known keys: description, input, request, headers.`, "unknown-tool-key", "warn");
1476
+ } else if (f.key === "description") {
1477
+ if (f.value !== "") descParts.push(f.value);
1478
+ const misplaced = new Set(shapedChildren(f.children, true).map((k) => k.line));
1479
+ for (const kid of shapedChildren(f.children, true)) {
1480
+ flag(kid.line, `This looks like a parameter, but it sits inside description:: text (deeper lines continue the description) \u2014 it would never reach the schema. Move it under "- input::" as: "${kid.rewrite}".`, "misplaced-param", "error");
1481
+ }
1482
+ for (const c of f.children) if (!misplaced.has(c.line)) descParts.push(c.text);
1483
+ } else if (f.key === "input") {
1484
+ for (const c of f.children) {
1485
+ const pm = c.text.match(PARAM_RE);
1486
+ if (pm) {
1487
+ const pname = (pm[1] ?? "").toLowerCase();
1488
+ if (pm[2] === "?") {
1489
+ flag(c.line, `Optional parameters are written with the "optional" prefix, never a "?" sigil. Replace this line with: "${rewriteRow(pname, "?", pm[3] ?? "")}".`, "malformed-param", "error");
1490
+ continue;
1491
+ }
1492
+ const parsed = parseParamValue(pm[3] ?? "");
1493
+ if (parsed.ok) {
1494
+ params.push({ name: pname, required: !parsed.optional && parsed.param.default === void 0, ...parsed.param });
1495
+ } else {
1496
+ flag(c.line, parsed.value !== void 0 ? `${parsed.reason} Replace this line with: "${rewriteRow(pname, "", parsed.value)}".` : parsed.reason, "malformed-param", "error");
1497
+ }
1498
+ continue;
1499
+ }
1500
+ const legacy = c.text.match(LEGACY_FIELD_RE);
1501
+ const bare = c.text.match(BARE_FIELD_RE);
1502
+ flag(
1503
+ c.line,
1504
+ legacy ? `Parameters use the definition marker "::" with a leading bullet, never a single colon. Replace this line with: "${rewriteRow(legacy[1] ?? "", legacy[2] ?? "", legacy[3] ?? "")}".` : bare ? `Every parameter is a bullet row under input:: (a sub-bullet is the schema's nesting). Replace this line with: "${rewriteRow(bare[1] ?? "", bare[2] ?? "", bare[3] ?? "")}".` : `This isn't a parameter. A parameter is "- name:: description." or "- name:: type: description." \u2014 the type is English, like "one of a, b" or "optional string". Found: "${c.text.slice(0, 60)}".`,
1505
+ "malformed-param",
1506
+ "error"
1507
+ );
1508
+ }
1509
+ } else if (f.key === "request") {
1510
+ const reqValue = stripPunct(f.value);
1511
+ const abs = reqValue.match(BINDING_ABS_RE);
1512
+ const via = reqValue.match(BINDING_SVC_RE);
1513
+ if (abs) {
1514
+ binding = { method: abs[1] ?? "GET", url: abs[2] ?? "", headers: [], line: f.line };
1515
+ } else if (via) {
1516
+ const svcName = (via[2] ?? "").toLowerCase();
1517
+ const service = services.get(svcName);
1518
+ if (!service) {
1519
+ flag(f.line, `Request references $SERVICE "${svcName}", which is not defined.`, "dangling-service", "error");
1520
+ } else {
1521
+ usedServices.add(svcName);
1522
+ binding = { method: via[1] ?? "GET", url: `${service.base}${via[3] ?? ""}`, headers: [], service: svcName, line: f.line };
1523
+ }
1524
+ } else {
1525
+ flag(f.line, `Malformed request in $TOOL ${name}. The value is "METHOD url": either absolute, like "- request:: GET https://api.example.com/orders", or through a $SERVICE, like "- request:: POST acme /orders" (acme names a $SERVICE). METHOD is uppercase: GET, POST, PUT, PATCH, DELETE.`, "malformed-binding", "error");
1526
+ }
1527
+ } else {
1528
+ for (const c of f.children) {
1529
+ const hm = c.text.match(HEADER_RE);
1530
+ if (hm) headers.push({ name: hm[1] ?? "", value: stripPunct(hm[2] ?? ""), line: c.line });
1531
+ else flag(c.line, `This isn't a header. A header is "- Header-Name:: value". Found: "${c.text.slice(0, 60)}".`, "malformed-header", "error");
1532
+ }
1533
+ }
1534
+ }
1535
+ const pseen = /* @__PURE__ */ new Set();
1536
+ for (const p of params) {
1537
+ if (pseen.has(p.name)) flag(block.line, `Parameter "${p.name}" is defined more than once in $TOOL ${name}.`, "duplicate-param", "error");
1538
+ pseen.add(p.name);
1539
+ }
1540
+ if (descParts.length === 0) {
1541
+ flag(block.line, `$TOOL ${name} has no description \u2014 the model has nothing to decide WHEN to call it.`, "tool-no-description", "warn");
1542
+ }
1543
+ if (binding) {
1544
+ const fromService = binding.service ? services.get(binding.service)?.headers ?? [] : [];
1545
+ const own = headers.map((h) => ({ name: h.name, value: h.value }));
1546
+ binding.headers = [...fromService.filter((sh) => !own.some((th) => th.name.toLowerCase() === sh.name.toLowerCase())), ...own];
1547
+ } else if (headers.length > 0) {
1548
+ flag(headers[0]?.line ?? block.line, `$TOOL ${name} has headers but no request \u2014 headers ride an HTTP binding. Add one, like: "- request:: GET https://api.example.com/orders", or delete the headers.`, "header-without-binding", "warn");
1549
+ }
1550
+ if (binding) {
1551
+ const paramNames = new Set(params.map((p) => p.name));
1552
+ checkPlaceholders(binding.url.slice(binding.service ? services.get(binding.service)?.base.length ?? 0 : 0), binding.line, paramNames, "the request", flag);
1553
+ for (const h of headers) {
1554
+ checkPlaceholders(h.value, h.line, paramNames, "this header", flag);
1555
+ if (!/\{env\./.test(h.value) && SECRET_RE.test(h.value)) {
1556
+ flag(h.line, `This looks like a credential literal \u2014 never put secrets in the file; use @{env.NAME} instead.`, "secret-literal", "warn");
1557
+ }
1558
+ }
1559
+ }
1560
+ tools.push({ name, description: descParts.join(" "), params, ...binding ? { binding } : {}, line: block.line });
1561
+ }
1562
+ const byName = /* @__PURE__ */ new Map();
1563
+ for (const tl of tools) {
1564
+ const list = byName.get(tl.name) ?? [];
1565
+ list.push(tl);
1566
+ byName.set(tl.name, list);
1567
+ }
1568
+ for (const [dup, list] of byName) {
1569
+ if (list.length > 1) conflicts.push({ lines: list.map((tl) => tl.line), message: `$TOOL ${dup} is defined ${list.length} times.`, code: "duplicate-tool", severity: "error" });
1570
+ }
1571
+ for (const s of svc.services) {
1572
+ if (!usedServices.has(s.name)) conflicts.push({ lines: [s.line], message: `$SERVICE ${s.name} is never used by any tool's request.`, code: "unused-service", severity: "warn" });
1573
+ }
1574
+ return { tools, services: svc.services, conflicts };
1575
+ }
1576
+ function toolInputSchema(tool) {
1577
+ const properties = {};
1578
+ for (const p of tool.params) {
1579
+ const base = { type: p.type };
1580
+ if (p.enum !== void 0) base["enum"] = p.enum;
1581
+ if (p.default !== void 0) base["default"] = p.default;
1582
+ if (p.description !== void 0) base["description"] = p.description;
1583
+ properties[p.name] = p.array ? { type: "array", items: { type: p.type, ...p.enum !== void 0 ? { enum: p.enum } : {} }, ...p.description !== void 0 ? { description: p.description } : {} } : base;
1584
+ }
1585
+ return { type: "object", properties, required: tool.params.filter((p) => p.required).map((p) => p.name) };
1586
+ }
1587
+
1588
+ // src/core/cardinality.ts
1589
+ var import_pluralize = __toESM(require_pluralize(), 1);
1590
+ import_pluralize.default.addUncountableRule("sms");
1591
+ var EXISTENTIAL_FRAME_SRC = "there\\s+(?:is|are|was|were|exists|exist)";
1592
+ var POSSESSIVE_FRAME_SRC = "(?:has|have)";
1593
+ function singularizeNoun(s) {
1594
+ return import_pluralize.default.singular(s.trim().toLowerCase());
1595
+ }
1596
+ function pluralizeNoun(s) {
1597
+ return import_pluralize.default.plural(s.trim().toLowerCase());
1598
+ }
1599
+ var numOf = (w) => NUMBER_WORDS[w] ?? (/^\d+$/.test(w) ? Number(w) : null);
1600
+ var FIXED = new Map(CARDINAL_QUANTITIES.map((q) => [q.phrase, { lo: q.lo, hi: q.hi ?? Infinity }]));
1601
+ var EXACT = new Set(EXACTNESS_MARKERS.map((m) => m.toLowerCase()));
1602
+ var QUANT_WORD_SET = new Set(QUANTIFIER_KEYWORDS.map((w) => w.toLowerCase()));
1603
+ function classifyCardinal(tokens) {
1604
+ const [a, b] = [tokens[0] ?? "", tokens[1] ?? ""];
1605
+ const fixed = FIXED.get(tokens.join(" "));
1606
+ if (fixed) return fixed;
1607
+ if (EXACT.has(a) && tokens.length === 2) {
1608
+ const n = numOf(b);
1609
+ return n === null ? null : { lo: n, hi: n };
1610
+ }
1611
+ if (a === "at" && b === "least" && tokens.length === 3) {
1612
+ const n = numOf(tokens[2] ?? "");
1613
+ return n === null ? null : { lo: n, hi: Infinity };
1614
+ }
1615
+ if (a === "at" && b === "most" && tokens.length === 3) {
1616
+ const n = numOf(tokens[2] ?? "");
1617
+ return n === null ? null : { lo: 0, hi: n };
1618
+ }
1619
+ if (tokens.length === 1) {
1620
+ const n = numOf(a);
1621
+ return n === null ? null : { lo: n, hi: Infinity };
1622
+ }
1623
+ return null;
1624
+ }
1625
+ function normTail(words) {
1626
+ return words.join(" ");
1627
+ }
1628
+ function quantityAndNoun(rest) {
1629
+ const words = rest.toLowerCase().split(/\s+/).map((w) => w.replace(/[.,;:!?]+$/, "")).filter((w) => w !== "");
1630
+ for (const take of [3, 2, 1]) {
1631
+ if (words.length <= take) continue;
1632
+ const iv = classifyCardinal(words.slice(0, take));
1633
+ if (iv === null) continue;
1634
+ const noun = singularizeNoun(words[take] ?? "");
1635
+ if (noun === "" || numOf(noun) !== null) return null;
1636
+ return { noun, lo: iv.lo, hi: iv.hi, text: words.slice(0, take + 1).join(" "), phrase: words.slice(0, take).join(" "), tail: normTail(words.slice(take + 1)) };
1637
+ }
1638
+ return null;
1639
+ }
1640
+ var THERE_RE = new RegExp(`^${EXISTENTIAL_FRAME_SRC}\\s+(.+)$`, "i");
1641
+ var SUBJECT_SRC = "(?:the\\s+|an?\\s+)?([A-Za-z][A-Za-z0-9_]*(?:\\s+[A-Za-z][A-Za-z0-9_]*)?)";
1642
+ var HAVE_RE = new RegExp(`^${SUBJECT_SRC}\\s+(${POSSESSIVE_FRAME_SRC})\\s+(.+)$`, "i");
1643
+ var MANY_CLASS = CARDINAL_QUANTITIES.filter((q) => q.lo === 2 && q.hi === null && !q.phrase.includes(" ")).map((q) => q.phrase);
1644
+ var ONE_OF_RE = new RegExp(`^${SUBJECT_SRC}\\s+(?:${copulaAlternation})\\s+one\\s+of\\s+(?:the\\s+)?((?:${MANY_CLASS.join("|")})|[a-z0-9]+)\\s+(.+)$`, "i");
1645
+ var THE_ONLY_RE = new RegExp(`^${SUBJECT_SRC}\\s+(?:${copulaAlternation})\\s+the\\s+only\\s+(.+)$`, "i");
1646
+ var subjectAbstains = (subject) => subject === "" || subject.split(/\s+/).some((w) => QUANT_WORD_SET.has(w) || numOf(w) !== null);
1647
+ function isCardinalQuantityLead(text) {
1648
+ const first = (text.trim().toLowerCase().split(/\s+/)[0] ?? "").replace(/[.,;:!?]+$/, "");
1649
+ return FIXED.has(first) || EXACT.has(first) || numOf(first) !== null;
1650
+ }
1651
+ function parseCardinalBody(body) {
1652
+ const trimmed = body.trim();
1653
+ const there = trimmed.match(THERE_RE);
1654
+ if (there) {
1655
+ const q = quantityAndNoun(there[1] ?? "");
1656
+ return q === null ? null : q;
1657
+ }
1658
+ const have = trimmed.match(HAVE_RE);
1659
+ if (have) {
1660
+ const owner = (have[1] ?? "").toLowerCase().trim();
1661
+ if (subjectAbstains(owner)) return null;
1662
+ const q = quantityAndNoun(have[3] ?? "");
1663
+ if (q === null) return null;
1664
+ if (q.phrase === "a" || q.phrase === "an") return null;
1665
+ return { ...q, owner, text: `${owner} ${(have[2] ?? "").toLowerCase()} ${q.text}` };
1666
+ }
1667
+ const only = trimmed.match(THE_ONLY_RE);
1668
+ if (only) {
1669
+ const subject = (only[1] ?? "").toLowerCase().trim();
1670
+ if (subjectAbstains(subject)) return null;
1671
+ const words = (only[2] ?? "").toLowerCase().split(/\s+/).map((w) => w.replace(/[.,;:!?]+$/, "")).filter((w) => w !== "");
1672
+ const noun = singularizeNoun(words[0] ?? "");
1673
+ if (noun === "" || numOf(noun) !== null) return null;
1674
+ return { noun, lo: 1, hi: 1, text: `the only ${words[0] ?? ""}`, phrase: "the only", tail: normTail(words.slice(1)) };
1675
+ }
1676
+ const oneOf = trimmed.match(ONE_OF_RE);
1677
+ if (oneOf) {
1678
+ const subject = (oneOf[1] ?? "").toLowerCase().trim();
1679
+ if (subjectAbstains(subject)) return null;
1680
+ const quantity = (oneOf[2] ?? "").toLowerCase();
1681
+ const n = numOf(quantity);
1682
+ const lo = n !== null ? Math.max(n, 2) : MANY_CLASS.includes(quantity) ? 2 : null;
1683
+ if (lo === null || n !== null && n < 2) return null;
1684
+ const words = (oneOf[3] ?? "").toLowerCase().split(/\s+/).map((w) => w.replace(/[.,;:!?]+$/, "")).filter((w) => w !== "");
1685
+ const noun = singularizeNoun(words[0] ?? "");
1686
+ if (noun === "" || numOf(noun) !== null) return null;
1687
+ return { noun, lo, hi: Infinity, text: `one of ${quantity} ${words[0] ?? ""}`, phrase: `one of ${quantity}`, tail: normTail(words.slice(1)) };
1688
+ }
1689
+ return null;
1690
+ }
1691
+ var NON_ACTION_LEADS = /* @__PURE__ */ new Set([
1692
+ "the",
1693
+ "a",
1694
+ "an",
1695
+ "this",
1696
+ "that",
1697
+ "these",
1698
+ "those",
1699
+ "it",
1700
+ "they",
1701
+ "we",
1702
+ "i",
1703
+ "he",
1704
+ "she",
1705
+ "you",
1706
+ "there",
1707
+ "is",
1708
+ "are",
1709
+ "was",
1710
+ "were",
1711
+ "am",
1712
+ "be",
1713
+ "been",
1714
+ "being",
1715
+ "if",
1716
+ "then",
1717
+ "otherwise",
1718
+ "and",
1719
+ "or",
1720
+ "not",
1721
+ "have",
1722
+ "has",
1723
+ "had"
1724
+ // the POSSESSIVE-have frame owns these (owner-scoped set sizes) — never double-read
1725
+ ]);
1726
+ function parseActionCount(segment) {
1727
+ const m = segment.trim().match(/^([A-Za-z][A-Za-z-]*)\s+(.+)$/);
1728
+ if (!m) return null;
1729
+ const verb = (m[1] ?? "").toLowerCase();
1730
+ if (NON_ACTION_LEADS.has(verb) || QUANT_WORD_SET.has(verb) || numOf(verb) !== null || FIXED.has(verb) || EXACT.has(verb)) return null;
1731
+ const q = quantityAndNoun(m[2] ?? "");
1732
+ if (q === null) return null;
1733
+ if (q.phrase === "a" || q.phrase === "an") return null;
1734
+ return { verb, noun: q.noun, lo: q.lo, hi: q.hi, text: `${verb} ${q.text}`, tail: q.tail };
1735
+ }
1736
+ function boundText(lo, hi) {
1737
+ if (lo === hi) return `exactly ${lo}`;
1738
+ if (hi === Infinity) return `${lo} or more`;
1739
+ if (lo === 0) return `at most ${hi}`;
1740
+ return `${lo} to ${hi}`;
1741
+ }
1742
+
1743
+ // src/core/possession.ts
1744
+ var DETERMINER = {
1745
+ my: "i",
1746
+ your: "you",
1747
+ its: "it",
1748
+ their: "they",
1749
+ our: "we",
1750
+ his: "he",
1751
+ her: "she",
1752
+ whose: "who"
1753
+ };
1754
+ var ABSOLUTE = {
1755
+ mine: "i",
1756
+ yours: "you",
1757
+ hers: "she",
1758
+ ours: "we",
1759
+ theirs: "they",
1760
+ his: "he",
1761
+ its: "it"
1762
+ };
1763
+ var NOTS = "((?:not\\s+)*)";
1764
+ var COP = `(${copulaAlternation})`;
1765
+ var DET_ALT = Object.keys(DETERMINER).join("|");
1766
+ var ABS_ALT = Object.keys(ABSOLUTE).join("|");
1767
+ var ART = "(?:(?:the|a|an)\\s+)?";
1768
+ var PRENOMINAL_DET = new RegExp(`\\b(${DET_ALT})\\s+(.+?)\\s+${COP}\\s+${NOTS}(.+)$`, "di");
1769
+ var PRENOMINAL_SAXON = new RegExp(`\\b${ART}(${HONORIFIC_PREFIX_SRC}${NAME_PHRASE})(?:'s|')\\s+(.+?)\\s+${COP}\\s+${NOTS}(.+)$`, "diu");
1770
+ var OF_GENITIVE = new RegExp(`^\\s*the\\s+(${NAME_PHRASE})\\s+of\\s+${ART}(${HONORIFIC_PREFIX_SRC}${NAME_PHRASE})\\s+${COP}\\s+${NOTS}(.+)$`, "diu");
1771
+ var HAVE_OF = new RegExp(`^\\s*${ART}(${HONORIFIC_PREFIX_SRC}${NAME_PHRASE})\\s+(?:has|have)\\s+${ART}(${NAME_PHRASE})\\s+of\\s+(.+)$`, "diu");
1772
+ var OWNERSHIP_ABS = new RegExp(`^\\s*${ART}(.+?)\\s+(?:${copulaAlternation})\\s+(${ABS_ALT})\\s*\\.?$`, "di");
1773
+ var OWNERSHIP_SAXON = new RegExp(`^\\s*${ART}(.+?)\\s+(?:${copulaAlternation})\\s+(${HONORIFIC_PREFIX_SRC}${NAME_PHRASE})(?:'s|')\\s*\\.?$`, "diu");
1774
+ function sliceAt(clean, m, i) {
1775
+ const ix = m.indices?.[i];
1776
+ return ix ? clean.slice(ix[0], ix[1]) : "";
1777
+ }
1778
+ function foldNegation(nots) {
1779
+ return (nots.match(/\bnot\b/gi) ?? []).length % 2 === 1;
1780
+ }
1781
+ function stripArticle(s) {
1782
+ return s.replace(/^(?:the|a|an)\s+/i, "").trim();
1783
+ }
1784
+ function splitPath(raw) {
1785
+ return raw.split(/'s\b\s*/).map((s) => s.trim()).filter((s) => s !== "");
1786
+ }
1787
+ function resolveOwnerPhrase(phrase) {
1788
+ const trimmed = phrase.trim();
1789
+ const of = trimmed.match(/^(.+?)\s+of\s+(.+)$/i);
1790
+ if (of) {
1791
+ const base = resolveOwnerPhrase((of[2] ?? "").trim().replace(/^(?:the|a|an)\s+/i, ""));
1792
+ return { owner: base.owner, prefix: [...base.prefix, (of[1] ?? "").trim()] };
1793
+ }
1794
+ const m = trimmed.match(/^([A-Za-z]+)\s+(.+)$/);
1795
+ const det = m ? (m[1] ?? "").toLowerCase() : "";
1796
+ if (m && DETERMINER[det]) return { owner: DETERMINER[det] ?? det, prefix: [(m[2] ?? "").trim()] };
1797
+ return { owner: trimmed, prefix: [] };
1798
+ }
1799
+ var CLAUSE_RE = new RegExp(`^(${NAME_PHRASE})\\s+(${copulaAlternation})\\s+(.+)$`, "iu");
1800
+ var PRONOUN_SUBJECTS = /* @__PURE__ */ new Set(["it", "this", "that", "they", "these", "those", "he", "she", "we", "i", "you"]);
1801
+ function makeProperties(owner, headPath, rawValue, headCopula, negated) {
1802
+ if (headPath.length === 0) return [];
1803
+ const conjuncts = stripQuotes(rawValue.trim()).split(/\s*,\s*|\s+and\s+/i).map((s) => s.trim()).filter((s) => s !== "");
1804
+ const out = [];
1805
+ let cur = { owner, path: headPath, many: /are/i.test(headCopula), values: [] };
1806
+ const flush = () => {
1807
+ const items = cur.values.map(stripPunct).filter((v) => v !== "");
1808
+ if (cur.path.length > 0 && items.length > 0) {
1809
+ const path = cur.path.map((p) => p.toLowerCase());
1810
+ out.push({ kind: "property", owner: cur.owner, path, items, negated, cardinality: cur.many || items.length > 1 ? "many" : "one" });
1811
+ }
1812
+ };
1813
+ conjuncts.forEach((c, i) => {
1814
+ if (i === 0) {
1815
+ cur.values.push(c);
1816
+ return;
1817
+ }
1818
+ const sub = parsePossessions(c)[0];
1819
+ if (sub) {
1820
+ flush();
1821
+ if (sub.kind === "property") {
1822
+ cur = { owner: sub.owner, path: sub.path, many: sub.cardinality === "many", values: [...sub.items] };
1823
+ } else {
1824
+ out.push(sub);
1825
+ cur = { owner, path: [], many: false, values: [] };
1826
+ }
1827
+ return;
1828
+ }
1829
+ const clause = c.match(CLAUSE_RE);
1830
+ if (clause) {
1831
+ const noun = clause[1]?.trim() ?? "";
1832
+ if (PRONOUN_SUBJECTS.has(noun.toLowerCase())) return;
1833
+ flush();
1834
+ cur = { owner, path: splitPath(noun), many: /are/i.test(clause[2] ?? "is"), values: [clause[3] ?? ""] };
1835
+ return;
1836
+ }
1837
+ cur.values.push(c);
1838
+ });
1839
+ flush();
1840
+ return out;
1841
+ }
1842
+ function parsePossessions(text) {
1843
+ const clean = stripInlineComment(text);
1844
+ if (!bindsHere(clean)) return [];
1845
+ const masked = maskQuotes(clean);
1846
+ let m = masked.match(HAVE_OF);
1847
+ if (m?.indices) {
1848
+ const { owner, prefix } = resolveOwnerPhrase(sliceAt(clean, m, 1));
1849
+ return makeProperties(owner, [...prefix, sliceAt(clean, m, 2).trim()], sliceAt(clean, m, 3), "", false);
1850
+ }
1851
+ m = masked.match(OF_GENITIVE);
1852
+ if (m?.indices) {
1853
+ const { owner, prefix } = resolveOwnerPhrase(sliceAt(clean, m, 2));
1854
+ return makeProperties(owner, [...prefix, sliceAt(clean, m, 1).trim()], sliceAt(clean, m, 5), sliceAt(clean, m, 3), foldNegation(sliceAt(clean, m, 4)));
1855
+ }
1856
+ m = masked.match(PRENOMINAL_DET);
1857
+ if (m?.indices) {
1858
+ const det = sliceAt(clean, m, 1).toLowerCase();
1859
+ return makeProperties(DETERMINER[det] ?? det, splitPath(sliceAt(clean, m, 2)), sliceAt(clean, m, 5), sliceAt(clean, m, 3), foldNegation(sliceAt(clean, m, 4)));
1860
+ }
1861
+ m = masked.match(PRENOMINAL_SAXON);
1862
+ if (m?.indices) {
1863
+ return makeProperties(sliceAt(clean, m, 1), splitPath(sliceAt(clean, m, 2)), sliceAt(clean, m, 5), sliceAt(clean, m, 3), foldNegation(sliceAt(clean, m, 4)));
1864
+ }
1865
+ m = masked.match(OWNERSHIP_ABS);
1866
+ if (m?.indices) {
1867
+ const abs = sliceAt(clean, m, 2).toLowerCase();
1868
+ return [{ kind: "ownership", owner: ABSOLUTE[abs] ?? abs, thing: stripArticle(sliceAt(clean, m, 1)) }];
1869
+ }
1870
+ m = masked.match(OWNERSHIP_SAXON);
1871
+ if (m?.indices) {
1872
+ return [{ kind: "ownership", owner: sliceAt(clean, m, 2), thing: stripArticle(sliceAt(clean, m, 1)) }];
1873
+ }
1874
+ return [];
1875
+ }
1876
+ function parsePossession(text) {
1877
+ return parsePossessions(text)[0] ?? null;
1878
+ }
1879
+
1880
+ // src/core/testblock.ts
1881
+ var TEST_HEADER = /^(\s*)\$TEST\s+([A-Za-z][A-Za-z0-9_-]*)\s*$/i;
1882
+ var FIELD = /^(\s*)-\s+([a-z][a-z0-9_]*)::\s*(.*)$/i;
1883
+ var SUBITEM = /^\s*-\s+(.+?)\s*$/;
1884
+ var ANY_TEST = /^\s*\$TEST\b/i;
1885
+ var RETIRED_EXPECT = /^\s*EXPECT\b/;
1886
+ var CONTAINS = /^contains\s+"(.+)"\s*$/i;
1887
+ var MATCHES = /^matches\s+\/(.+)\/([a-z]*)\s*$/i;
1888
+ var BOUND = /^at\s+(most|least)\s+(\d+)\s+(sentences?|words?|characters?)\s*$/i;
1889
+ var leading2 = (s) => s.length - s.trimStart().length;
1890
+ function parseExpect(text) {
1891
+ const t = text.trim();
1892
+ if (t === "") return { assert: null, error: "an expectation has no condition after it." };
1893
+ const c = CONTAINS.exec(t);
1894
+ if (c) return { assert: { kind: "contains", value: c[1] ?? "" }, error: null };
1895
+ const m = MATCHES.exec(t);
1896
+ if (m) {
1897
+ try {
1898
+ void new RegExp(m[1] ?? "", m[2] ?? "");
1899
+ return { assert: { kind: "matches", source: m[1] ?? "", flags: m[2] ?? "" }, error: null };
1900
+ } catch {
1901
+ return { assert: null, error: `the expectation \`matches /${m[1] ?? ""}/\` is not a valid regular expression.` };
1902
+ }
1903
+ }
1904
+ const b = BOUND.exec(t);
1905
+ if (b) {
1906
+ const unit = (b[3] ?? "").toLowerCase().replace(/s?$/, "s");
1907
+ return { assert: { kind: b[1]?.toLowerCase() === "most" ? "atMost" : "atLeast", n: Number(b[2]), unit }, error: null };
1908
+ }
1909
+ return { assert: { kind: "rubric", text: t }, error: null };
1910
+ }
1911
+ function parseTests(lines) {
1912
+ const blocks = [];
1913
+ const seen = /* @__PURE__ */ new Set();
1914
+ let i = 0;
1915
+ while (i < lines.length) {
1916
+ const m = (lines[i] ?? "").match(TEST_HEADER);
1917
+ if (!m) {
1918
+ i += 1;
1919
+ continue;
1920
+ }
1921
+ const indent = (m[1] ?? "").length;
1922
+ const id = (m[2] ?? "").toLowerCase();
1923
+ let j = i + 1;
1924
+ while (j < lines.length) {
1925
+ const l = lines[j] ?? "";
1926
+ if (l.trim() !== "" && leading2(l) <= indent) break;
1927
+ j += 1;
1928
+ }
1929
+ const blockEnd = j;
1930
+ let input = null;
1931
+ const expects = [];
1932
+ const rubricLines = [];
1933
+ let error = null;
1934
+ let k = i + 1;
1935
+ while (k < blockEnd && error === null) {
1936
+ const raw = lines[k] ?? "";
1937
+ if (raw.trim() === "") {
1938
+ k += 1;
1939
+ continue;
1940
+ }
1941
+ if (ANY_TEST.test(raw)) {
1942
+ error = "a nested $TEST is not supported \u2014 dedent it to start a new test.";
1943
+ break;
1944
+ }
1945
+ const fm = raw.match(FIELD);
1946
+ if (!fm) {
1947
+ error = `only "- input::" and "- expect::" fields belong in $TEST ${id} \u2014 "${raw.trim().slice(0, 48)}" is not one.`;
1948
+ break;
1949
+ }
1950
+ const fieldIndent = (fm[1] ?? "").length;
1951
+ const key = (fm[2] ?? "").toLowerCase();
1952
+ const value = (fm[3] ?? "").trim();
1953
+ if (key === "input") {
1954
+ if (value === "") {
1955
+ error = `$TEST ${id} needs the user turn on the "- input::" line.`;
1956
+ break;
1957
+ }
1958
+ input = value;
1959
+ k += 1;
1960
+ } else if (key === "expect") {
1961
+ if (value !== "") {
1962
+ error = `"- expect::" is a list \u2014 put each expectation on its own "- " sub-bullet beneath it.`;
1963
+ break;
1964
+ }
1965
+ k += 1;
1966
+ let count = 0;
1967
+ while (k < blockEnd) {
1968
+ const sub = lines[k] ?? "";
1969
+ if (sub.trim() === "") {
1970
+ k += 1;
1971
+ continue;
1972
+ }
1973
+ if (leading2(sub) <= fieldIndent) break;
1974
+ const sm = sub.match(SUBITEM);
1975
+ if (sm === null) {
1976
+ error = `each expectation is a "- " sub-bullet under "- expect::" \u2014 "${sub.trim().slice(0, 48)}" is not.`;
1977
+ break;
1978
+ }
1979
+ const parsed = parseExpect(sm[1] ?? "");
1980
+ if (parsed.error !== null) {
1981
+ error = parsed.error;
1982
+ break;
1983
+ }
1984
+ if (parsed.assert) {
1985
+ expects.push(parsed.assert);
1986
+ if (parsed.assert.kind === "rubric") rubricLines.push({ line: k, start: (sub.match(/^\s*-\s+/)?.[0] ?? "").length, text: parsed.assert.text });
1987
+ }
1988
+ count += 1;
1989
+ k += 1;
1990
+ }
1991
+ if (error === null && count === 0) error = `$TEST ${id}'s "- expect::" has no expectations \u2014 add a "- " sub-bullet.`;
1992
+ } else {
1993
+ error = `only "- input::" and "- expect::" fields belong in $TEST ${id} \u2014 "- ${key}::" is neither.`;
1994
+ break;
1995
+ }
1996
+ }
1997
+ if (error === null && input === null) error = `$TEST ${id} needs an "- input:: \u2026" line.`;
1998
+ if (error === null && expects.length === 0) error = `$TEST ${id} needs at least one expectation under "- expect::".`;
1999
+ if (error === null && seen.has(id)) error = `duplicate $TEST id "${id}".`;
2000
+ seen.add(id);
2001
+ blocks.push({ headerLine: i, blockEnd, indent, id, input: input ?? "", expects, rubricLines, error });
2002
+ i = blockEnd;
2003
+ }
2004
+ return blocks;
2005
+ }
2006
+ function testBlockLines(lines) {
2007
+ const out = /* @__PURE__ */ new Set();
2008
+ for (const b of parseTests(lines)) for (let i = b.headerLine; i < b.blockEnd; i += 1) out.add(i);
2009
+ return out;
2010
+ }
2011
+ function rubricExpectLines(lines) {
2012
+ return parseTests(lines).filter((b) => b.error === null).flatMap((b) => b.rubricLines);
2013
+ }
2014
+ function strayExpects(lines) {
2015
+ const out = [];
2016
+ lines.forEach((raw, i) => {
2017
+ if (RETIRED_EXPECT.test(raw)) out.push(i);
2018
+ });
2019
+ return out;
2020
+ }
2021
+ var slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48) || "expectation";
2022
+ function extractSuite(source) {
2023
+ const blocks = parseTests(source.replace(/\r\n/g, "\n").split("\n")).filter((b) => b.error === null);
2024
+ const testCases = [];
2025
+ const caseRubrics = {};
2026
+ const asserts = {};
2027
+ const union = /* @__PURE__ */ new Map();
2028
+ for (const b of blocks) {
2029
+ const rubricTexts = b.expects.filter((e) => e.kind === "rubric");
2030
+ const detAsserts = b.expects.filter((e) => e.kind !== "rubric");
2031
+ const dims = rubricTexts.map((r) => ({ key: slug(r.text), description: r.text }));
2032
+ for (const d of dims) if (!union.has(d.key)) union.set(d.key, d);
2033
+ testCases.push({
2034
+ id: b.id,
2035
+ input: b.input,
2036
+ expectation: rubricTexts.length > 0 ? rubricTexts.map((r) => r.text).join("; ") : void 0
2037
+ });
2038
+ caseRubrics[b.id] = { dimensions: dims };
2039
+ asserts[b.id] = detAsserts;
2040
+ }
2041
+ return { testCases, rubric: { dimensions: [...union.values()] }, caseRubrics, asserts };
2042
+ }
2043
+
2044
+ // src/core/exampleblock.ts
2045
+ var EXAMPLE_HEADER = /^(\s*)\$EXAMPLE\s+([A-Za-z][A-Za-z0-9_-]*)\s*$/i;
2046
+ var FIELD2 = /^(\s*)-\s+([a-z][a-z0-9_]*)::\s*(.*)$/i;
2047
+ var ANY_EXAMPLE = /^\s*\$EXAMPLE\b/i;
2048
+ var leading3 = (s) => s.length - s.trimStart().length;
2049
+ function parseExamples(lines) {
2050
+ const blocks = [];
2051
+ const seen = /* @__PURE__ */ new Set();
2052
+ let i = 0;
2053
+ while (i < lines.length) {
2054
+ const m = (lines[i] ?? "").match(EXAMPLE_HEADER);
2055
+ if (!m) {
2056
+ i += 1;
2057
+ continue;
2058
+ }
2059
+ const indent = (m[1] ?? "").length;
2060
+ const id = (m[2] ?? "").toLowerCase();
2061
+ let j = i + 1;
2062
+ while (j < lines.length) {
2063
+ const l = lines[j] ?? "";
2064
+ if (l.trim() !== "" && leading3(l) <= indent) break;
2065
+ j += 1;
2066
+ }
2067
+ const blockEnd = j;
2068
+ let input = null;
2069
+ let good = null;
2070
+ let bad = null;
2071
+ let error = null;
2072
+ let k = i + 1;
2073
+ while (k < blockEnd && error === null) {
2074
+ const raw = lines[k] ?? "";
2075
+ if (raw.trim() === "") {
2076
+ k += 1;
2077
+ continue;
2078
+ }
2079
+ if (ANY_EXAMPLE.test(raw)) {
2080
+ error = "a nested $EXAMPLE is not supported \u2014 dedent it to start a new example.";
2081
+ break;
2082
+ }
2083
+ const fm = raw.match(FIELD2);
2084
+ if (!fm) {
2085
+ error = `only "- input::", "- good::", and "- bad::" fields belong in $EXAMPLE ${id} \u2014 "${raw.trim().slice(0, 48)}" is not one.`;
2086
+ break;
2087
+ }
2088
+ const key = (fm[2] ?? "").toLowerCase();
2089
+ const value = (fm[3] ?? "").trim();
2090
+ if (key === "input" || key === "good" || key === "bad") {
2091
+ if (value === "") {
2092
+ error = `$EXAMPLE ${id}'s "- ${key}::" needs its text on the same line.`;
2093
+ break;
2094
+ }
2095
+ if (key === "input") input = value;
2096
+ else if (key === "good") good = value;
2097
+ else bad = value;
2098
+ k += 1;
2099
+ } else {
2100
+ error = `only "- input::", "- good::", and "- bad::" fields belong in $EXAMPLE ${id} \u2014 "- ${key}::" is neither.`;
2101
+ break;
2102
+ }
2103
+ }
2104
+ if (error === null && input === null) error = `$EXAMPLE ${id} needs an "- input:: \u2026" line.`;
2105
+ if (error === null && good === null) error = `$EXAMPLE ${id} needs a "- good:: \u2026" line \u2014 the ideal response to demonstrate.`;
2106
+ if (error === null && seen.has(id)) error = `duplicate $EXAMPLE id "${id}".`;
2107
+ seen.add(id);
2108
+ blocks.push({ headerLine: i, blockEnd, indent, id, input: input ?? "", good: good ?? "", bad, error });
2109
+ i = blockEnd;
2110
+ }
2111
+ return blocks;
2112
+ }
2113
+ function exampleBlockLines(lines) {
2114
+ const out = /* @__PURE__ */ new Set();
2115
+ for (const b of parseExamples(lines)) for (let i = b.headerLine; i < b.blockEnd; i += 1) out.add(i);
2116
+ return out;
2117
+ }
2118
+ function renderExample(b) {
2119
+ if (b.error !== null) return [];
2120
+ return ["<example>", `User: ${b.input}`, `Assistant: ${b.good}`, "</example>"];
2121
+ }
2122
+
2123
+ // src/core/prompt.ts
2124
+ function classifyLines(rawLines) {
2125
+ const kinds = [];
2126
+ let inBlock = false;
2127
+ let inFence = false;
2128
+ for (const raw of rawLines) {
2129
+ const t = raw.trim();
2130
+ const isFence = t.startsWith("```") || t.startsWith("~~~");
2131
+ if (inFence) {
2132
+ if (isFence) inFence = false;
2133
+ kinds.push(t === "" ? "blank" : "content");
2134
+ continue;
2135
+ }
2136
+ if (inBlock) {
2137
+ kinds.push("comment");
2138
+ if (t.includes("*/")) inBlock = false;
2139
+ continue;
2140
+ }
2141
+ if (isFence) {
2142
+ inFence = true;
2143
+ kinds.push("content");
2144
+ continue;
2145
+ }
2146
+ if (t.startsWith("/*")) {
2147
+ kinds.push("comment");
2148
+ if (!t.includes("*/")) inBlock = true;
2149
+ continue;
2150
+ }
2151
+ if (t.startsWith("//")) {
2152
+ kinds.push("comment");
2153
+ continue;
2154
+ }
2155
+ kinds.push(t === "" ? "blank" : "content");
2156
+ }
2157
+ return kinds;
2158
+ }
2159
+ function isHeadingLine(text) {
2160
+ return /^\s*#{1,6}\s/.test(text);
2161
+ }
2162
+ function fenceMask(rawLines) {
2163
+ const mask = [];
2164
+ let inFence = false;
2165
+ for (const raw of rawLines) {
2166
+ const t = raw.trim();
2167
+ const isFence = t.startsWith("```") || t.startsWith("~~~");
2168
+ if (inFence) {
2169
+ mask.push(true);
2170
+ if (isFence) inFence = false;
2171
+ } else if (isFence) {
2172
+ inFence = true;
2173
+ mask.push(true);
2174
+ } else {
2175
+ mask.push(false);
2176
+ }
2177
+ }
2178
+ return mask;
2179
+ }
2180
+ function stripIndent(source) {
2181
+ const lines = source.split("\n");
2182
+ const mask = fenceMask(lines);
2183
+ const tools = toolBlockMask(lines);
2184
+ const switches = switchBlockMask(lines);
2185
+ const defs = defListMask(lines);
2186
+ const dedent = new Array(lines.length).fill(0);
2187
+ const markRuns = (m) => {
2188
+ for (let i = 0; i < lines.length; i += 1) {
2189
+ if (m[i] !== true || i > 0 && m[i - 1] === true) continue;
2190
+ const anchor = ((lines[i] ?? "").match(/^ */) ?? [""])[0].length;
2191
+ for (let j = i; j < lines.length && m[j] === true; j += 1) dedent[j] = anchor;
2192
+ }
2193
+ };
2194
+ markRuns(tools);
2195
+ markRuns(switches);
2196
+ markRuns(defs);
2197
+ return lines.map((l, i) => {
2198
+ if (mask[i]) return l;
2199
+ if (tools[i] || switches[i] || defs[i]) {
2200
+ const lead = (l.match(/^ */) ?? [""])[0].length;
2201
+ return l.slice(Math.min(dedent[i] ?? 0, lead));
2202
+ }
2203
+ return l.replace(/^[ \t]+/, "");
2204
+ }).join("\n");
2205
+ }
2206
+ function matchDeclaration(re, text) {
2207
+ const m = maskQuotes(text).match(re);
2208
+ if (!m?.indices) return null;
2209
+ const at = (i) => {
2210
+ const ix = m.indices?.[i];
2211
+ return ix ? text.slice(ix[0], ix[1]) : "";
2212
+ };
2213
+ const nots = (at(2).match(/NOT/gi) ?? []).length;
2214
+ return { name: at(1), negated: nots % 2 === 1, value: stripQuotes(stripPunct(at(3))) };
2215
+ }
2216
+ var ANYWHERE_DECLARATION_RE = new RegExp(
2217
+ `\\b(${HONORIFIC_PREFIX_SRC}${NAME_WORD})${copulaConnector}\\s+((?:(?:NOT|not)\\s+)*)(.+)$`,
2218
+ "du"
2219
+ );
2220
+ var QUANTIFIER_LED_RE = new RegExp(`\\b(?:${QUANTIFIER_KEYWORDS.map((w) => w.toLowerCase()).join("|")})\\s+$`, "i");
2221
+ function findDeclaration(text) {
2222
+ if (isHeadingLine(text)) return null;
2223
+ if (!bindsHere(text)) return null;
2224
+ if (parsePossession(text)) return null;
2225
+ const m = matchDeclaration(ANYWHERE_DECLARATION_RE, text);
2226
+ if (!m) return null;
2227
+ if (m.name.toLowerCase() === "there") return null;
2228
+ const at = maskQuotes(text).match(ANYWHERE_DECLARATION_RE)?.indices?.[1]?.[0] ?? 0;
2229
+ if (QUANTIFIER_LED_RE.test(text.slice(0, at))) return null;
2230
+ return m;
2231
+ }
2232
+ var INCLUDING_RE = new RegExp(`\\b(${NAME_WORD})\\s+(?:INCLUDING|including)\\s+(.+)$`, "du");
2233
+ function findDeclaredItems(text) {
2234
+ const clean = stripInlineComment(text);
2235
+ if (!bindsHere(clean)) return null;
2236
+ const inc = maskQuotes(clean).match(INCLUDING_RE);
2237
+ if (inc?.indices) {
2238
+ const nameIx = inc.indices[1];
2239
+ const listIx = inc.indices[2];
2240
+ const name = nameIx ? clean.slice(nameIx[0], nameIx[1]) : "";
2241
+ const list = listIx ? clean.slice(listIx[0], listIx[1]) : "";
2242
+ const items2 = stripQuotes(stripPunct(list)).split(",").map((s) => s.trim()).filter((s) => s !== "");
2243
+ return { name, items: items2, negated: false, list: true };
2244
+ }
2245
+ const d = findDeclaration(clean);
2246
+ if (!d) return null;
2247
+ const items = d.value.split(/\s+AND\s+/i).map((s) => s.trim()).filter((s) => s !== "");
2248
+ return { name: d.name, items, negated: d.negated };
2249
+ }
2250
+ var BULLET_RE = /^\s*(?:[-*+]|\d+[.)])\s+(.+)$/;
2251
+ var LIST_HEADER_RE = new RegExp(`^(${NAME_PHRASE})\\s+(?:is|are|am)(\\s+not)?\\s*:\\s*$`, "iu");
2252
+ function pushDecls(seg, declText, i, out) {
2253
+ const possessions = parsePossessions(seg);
2254
+ if (possessions.length > 0) {
2255
+ for (const poss of possessions) {
2256
+ out.push(
2257
+ poss.kind === "property" ? { line: i, name: poss.owner, items: poss.items, negated: poss.negated, path: poss.path, cardinality: poss.cardinality } : { line: i, name: poss.owner, items: [], negated: false, owns: poss.thing }
2258
+ );
2259
+ }
2260
+ return true;
2261
+ }
2262
+ const d = findDeclaredItems(declText);
2263
+ if (d) {
2264
+ out.push({ line: i, name: d.name, items: d.items, negated: d.negated, ...d.list ? { list: true } : {} });
2265
+ return true;
2266
+ }
2267
+ return false;
2268
+ }
2269
+ function resolveDeclarations(rawLines) {
2270
+ const out = [];
2271
+ let header = null;
2272
+ const toolMask = toolBlockMask(rawLines);
2273
+ rawLines.forEach((raw, i) => {
2274
+ if (toolMask[i]) return;
2275
+ const trimmed = raw.trim();
2276
+ if (trimmed === "" || isHeadingLine(trimmed)) {
2277
+ header = null;
2278
+ return;
2279
+ }
2280
+ const bullet = raw.match(BULLET_RE);
2281
+ const clause = stripInlineComment(bullet ? bullet[1] ?? "" : trimmed).trim();
2282
+ if (bullet && header && clause !== "") {
2283
+ out.push({ line: i, name: header.name, items: [stripPunct(clause)], negated: header.negated });
2284
+ return;
2285
+ }
2286
+ if (!bullet) {
2287
+ const hm = clause.match(LIST_HEADER_RE);
2288
+ if (hm) {
2289
+ header = { name: (hm[1] ?? "").trim(), negated: Boolean(hm[2]) };
2290
+ out.push({ line: i, name: header.name, items: [], negated: header.negated });
2291
+ return;
2292
+ }
2293
+ }
2294
+ const sentences = splitSentences(clause);
2295
+ if (sentences.length > 1) {
2296
+ let matched = false;
2297
+ for (const s of sentences) if (pushDecls(s.text, s.text, i, out)) matched = true;
2298
+ if (matched) {
2299
+ header = null;
2300
+ return;
2301
+ }
2302
+ } else if (pushDecls(clause, raw, i, out)) {
2303
+ header = null;
2304
+ return;
2305
+ }
2306
+ if (!bullet) header = null;
2307
+ });
2308
+ return out;
2309
+ }
2310
+ function properNounAlias(value) {
2311
+ const first = (value.split(",")[0] ?? "").trim();
2312
+ if (first === "" || /^(?:a|an|the)\s/i.test(first)) return null;
2313
+ return /^\p{Lu}[\p{L}.]*(?:\s+\p{Lu}[\p{L}.]*)*$/u.test(first) ? first : null;
2314
+ }
2315
+ function declaredObjectNames(decls) {
2316
+ return new Set(decls.filter((d) => !d.negated && !d.path && !d.owns).map((d) => d.name.toLowerCase()));
2317
+ }
2318
+ function objectRefAlias(value, declaredNames) {
2319
+ const first = (value.split(",")[0] ?? "").trim();
2320
+ const key = stripPunct(first).toLowerCase();
2321
+ return first !== "" && UNHIGHLIGHTED_NAMES.has(key) && declaredNames.has(key) ? first : null;
2322
+ }
2323
+ function aliasMap(decls) {
2324
+ const direct = /* @__PURE__ */ new Map();
2325
+ const declaredNames = declaredObjectNames(decls);
2326
+ for (const d of decls) {
2327
+ if (d.negated || d.path || d.owns) continue;
2328
+ for (const item of d.items) {
2329
+ const nm = properNounAlias(item) ?? objectRefAlias(item, declaredNames);
2330
+ if (!nm) continue;
2331
+ const ak = stripPunct(nm).toLowerCase();
2332
+ const ck = d.name.toLowerCase();
2333
+ if (ak !== ck && !direct.has(ak)) direct.set(ak, ck);
2334
+ }
2335
+ }
2336
+ const canon = (k) => {
2337
+ const seen = /* @__PURE__ */ new Set();
2338
+ let cur = k;
2339
+ while (direct.has(cur) && !seen.has(cur)) {
2340
+ seen.add(cur);
2341
+ cur = direct.get(cur) ?? cur;
2342
+ }
2343
+ return cur;
2344
+ };
2345
+ const resolved = /* @__PURE__ */ new Map();
2346
+ for (const k of direct.keys()) resolved.set(k, canon(k));
2347
+ return resolved;
2348
+ }
2349
+ function collectObjects(rawLines) {
2350
+ const literal = examplesMask(rawLines);
2351
+ const decls = resolveDeclarations(rawLines.map((l, i) => literal[i] ? "" : stripAnnotation(l)));
2352
+ const aliases = aliasMap(decls);
2353
+ const declaredNames = declaredObjectNames(decls);
2354
+ const canon = (key) => aliases.get(key) ?? key;
2355
+ const objects = /* @__PURE__ */ new Map();
2356
+ for (const d of decls) {
2357
+ if (d.negated) continue;
2358
+ const subjKey = d.name.toLowerCase();
2359
+ const key = canon(subjKey);
2360
+ let obj = objects.get(key);
2361
+ if (!obj) {
2362
+ obj = { name: d.name, items: [], properties: /* @__PURE__ */ new Map(), owns: [], aliases: [], lines: [] };
2363
+ objects.set(key, obj);
2364
+ }
2365
+ if (subjKey === key) obj.name = d.name;
2366
+ if (!obj.lines.includes(d.line)) obj.lines.push(d.line);
2367
+ if (d.owns) {
2368
+ if (!obj.owns.includes(d.owns)) obj.owns.push(d.owns);
2369
+ } else if (d.path) {
2370
+ const pathKey = d.path.join(".");
2371
+ const pv = obj.properties.get(pathKey) ?? { values: [], cardinality: "one", lines: [] };
2372
+ if (d.cardinality === "many") pv.cardinality = "many";
2373
+ for (const it of d.items) if (!pv.values.includes(it)) pv.values.push(it);
2374
+ if (!pv.lines.includes(d.line)) pv.lines.push(d.line);
2375
+ obj.properties.set(pathKey, pv);
2376
+ } else {
2377
+ if (d.list) {
2378
+ const members = obj.listMembers ?? [];
2379
+ for (const it of d.items) if (!members.includes(it)) members.push(it);
2380
+ obj.listMembers = members;
2381
+ }
2382
+ for (const it of d.items) {
2383
+ const nm = properNounAlias(it) ?? objectRefAlias(it, declaredNames);
2384
+ if (nm) {
2385
+ const alias = stripPunct(nm);
2386
+ if (alias.toLowerCase() !== key && !obj.aliases.includes(alias)) obj.aliases.push(alias);
2387
+ const rest = it.slice(nm.length).replace(/^\s*,\s*/, "").trim();
2388
+ if (rest !== "" && !obj.items.includes(rest)) obj.items.push(rest);
2389
+ } else if (!obj.items.includes(it)) {
2390
+ obj.items.push(it);
2391
+ }
2392
+ }
2393
+ }
2394
+ }
2395
+ rawLines.forEach((raw, i) => {
2396
+ if (literal[i]) return;
2397
+ const line = stripInlineComment(stripAnnotation(raw));
2398
+ const text = line.replace(BULLET_RE, "$1").trim();
2399
+ if (text === "" || text.startsWith("//") || isHeadingLine(text)) return;
2400
+ if (/^\$?(?:IF|ELSE)\b/i.test(text) || /\bIF\b.+\bTHEN\b/i.test(text)) return;
2401
+ const bound = parseCardinalBody(maskQuotes(text));
2402
+ if (!bound) return;
2403
+ if (bound.tail !== "") return;
2404
+ if (bound.owner !== void 0) {
2405
+ const ownerKey = canon(bound.owner);
2406
+ let owner = objects.get(ownerKey);
2407
+ if (!owner) {
2408
+ owner = { name: bound.owner, items: [], properties: /* @__PURE__ */ new Map(), owns: [], aliases: [], lines: [] };
2409
+ objects.set(ownerKey, owner);
2410
+ }
2411
+ if (!owner.lines.includes(i)) owner.lines.push(i);
2412
+ const pv = owner.properties.get(bound.noun) ?? { values: [], cardinality: "one", lines: [] };
2413
+ const plo = Math.max(pv.count?.lo ?? 0, bound.lo);
2414
+ const phi = Math.min(pv.count?.hi ?? Infinity, bound.hi);
2415
+ pv.count = plo <= phi ? { lo: plo, hi: phi, lines: [...pv.count?.lines ?? [], i] } : { lo: pv.count?.lo ?? bound.lo, hi: pv.count?.hi ?? bound.hi, lines: [...pv.count?.lines ?? [], i] };
2416
+ if (!pv.lines.includes(i)) pv.lines.push(i);
2417
+ owner.properties.set(bound.noun, pv);
2418
+ return;
2419
+ }
2420
+ const candidates = [bound.noun, pluralizeNoun(bound.noun)].map((k) => canon(k));
2421
+ const key = candidates.find((k) => objects.has(k)) ?? bound.noun;
2422
+ let obj = objects.get(key);
2423
+ if (!obj) {
2424
+ obj = { name: bound.noun, items: [], properties: /* @__PURE__ */ new Map(), owns: [], aliases: [], lines: [] };
2425
+ objects.set(key, obj);
2426
+ }
2427
+ if (!obj.lines.includes(i)) obj.lines.push(i);
2428
+ const prev = obj.count;
2429
+ const lo = Math.max(prev?.lo ?? 0, bound.lo);
2430
+ const hi = Math.min(prev?.hi ?? Infinity, bound.hi);
2431
+ obj.count = lo <= hi ? { lo, hi, lines: [...prev?.lines ?? [], i] } : { lo: prev?.lo ?? bound.lo, hi: prev?.hi ?? bound.hi, lines: [...prev?.lines ?? [], i] };
2432
+ });
2433
+ return objects;
2434
+ }
2435
+ var UNHIGHLIGHTED_NAMES = /* @__PURE__ */ new Set([
2436
+ "you",
2437
+ "your",
2438
+ "yours",
2439
+ "i",
2440
+ "me",
2441
+ "my",
2442
+ "mine",
2443
+ "we",
2444
+ "us",
2445
+ "our",
2446
+ "ours",
2447
+ "it",
2448
+ "its",
2449
+ "they",
2450
+ "them",
2451
+ "their",
2452
+ "theirs",
2453
+ "he",
2454
+ "him",
2455
+ "his",
2456
+ "she",
2457
+ "her",
2458
+ "hers",
2459
+ "this",
2460
+ "that",
2461
+ "these",
2462
+ "those"
2463
+ ]);
2464
+ var NEVER_ADDRESSEE = /* @__PURE__ */ new Set(["user", "customer", "caller", "human", "visitor", "they", "them"]);
2465
+ function addresseeSubjects(rawLines, extra = []) {
2466
+ const decls = resolveDeclarations(rawLines);
2467
+ const aliases = aliasMap(decls);
2468
+ const out = /* @__PURE__ */ new Set();
2469
+ const add = (phrase) => {
2470
+ const stripped = phrase.trim().toLowerCase().replace(/^(?:the|a|an)\s+/, "").replace(/[.,;:!?]+$/, "").trim();
2471
+ if (stripped === "" || stripped === "you") return;
2472
+ const words = stripped.split(/\s+/);
2473
+ const last = words[words.length - 1] ?? "";
2474
+ if (NEVER_ADDRESSEE.has(stripped) || NEVER_ADDRESSEE.has(singularizeNoun(last))) return;
2475
+ out.add(stripped);
2476
+ const other = singularizeNoun(last) === last ? pluralizeNoun(last) : singularizeNoun(last);
2477
+ if (other !== last && other.length > 1) out.add([...words.slice(0, -1), other].join(" "));
2478
+ };
2479
+ for (const [alias, target] of aliases) if (target === "you") add(alias);
2480
+ for (const d of decls) {
2481
+ const canon = aliases.get(d.name.toLowerCase()) ?? d.name.toLowerCase();
2482
+ if (canon !== "you" || d.negated) continue;
2483
+ for (const item of d.items) if (/^an?\s+/i.test(item)) add(item);
2484
+ }
2485
+ for (const e of extra) add(e);
2486
+ return out;
2487
+ }
2488
+ function findEnumDeclaration(text) {
2489
+ const d = findDeclaration(stripInlineComment(text));
2490
+ if (!d || d.negated) return null;
2491
+ const m = d.value.match(/^one\s+of\s*(:)?\s*(.+)$/i);
2492
+ if (!m) return null;
2493
+ const members = (m[2] ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
2494
+ if (m[1] === void 0 && members.length === 1 && isCardinalQuantityLead(members[0] ?? "")) return null;
2495
+ return members.length > 0 ? { name: d.name, members } : null;
2496
+ }
2497
+ function expandToolRefs(text, tools, imported) {
2498
+ if (tools.size === 0 && (imported?.size ?? 0) === 0) return text;
2499
+ const standalone = /^\s*(?:[-*]\s+)?@\[[a-z][a-z0-9_]*\]\s*\.?\s*$/i.test(text);
2500
+ return text.replace(/@\[([a-z][a-z0-9_]*)\]/gi, (whole, name) => {
2501
+ const key = name.toLowerCase();
2502
+ if (tools.has(key)) {
2503
+ const desc = tools.get(key) ?? "";
2504
+ return standalone && desc !== "" ? `${name} \u2014 ${desc}` : name;
2505
+ }
2506
+ return imported?.has(key) === true ? name : whole;
2507
+ });
2508
+ }
2509
+ function expandRefs(text, decls) {
2510
+ let cur = text;
2511
+ for (let pass = 0; pass < 5; pass += 1) {
2512
+ const next = cur.replace(/@\{([A-Za-z0-9_.]+)\}/g, (_whole, name) => decls.get(name.toLowerCase()) ?? `{${name}}`);
2513
+ if (next === cur) break;
2514
+ cur = next;
2515
+ }
2516
+ return cur;
2517
+ }
2518
+ var COMPILED_LIST_ITEM = /^\s*(?:[-*+]\s|(?:\d{1,9}|[a-z])[.)]\s)/i;
2519
+ function classifyCompiledLine(text) {
2520
+ if (text.trim() === "") return "blank";
2521
+ const t = text.trimStart();
2522
+ if (t.startsWith("<") || t.startsWith("#") || COMPILED_LIST_ITEM.test(text)) return "keep";
2523
+ return "flow";
2524
+ }
2525
+ function joinCompiledLines(lines, kinds) {
2526
+ const res = [];
2527
+ let buf = null;
2528
+ let bufIndent = "";
2529
+ let pendingBlank = false;
2530
+ const flush = () => {
2531
+ if (buf !== null) {
2532
+ res.push(buf);
2533
+ buf = null;
2534
+ bufIndent = "";
2535
+ }
2536
+ };
2537
+ for (let i = 0; i < lines.length; i += 1) {
2538
+ const text = lines[i] ?? "";
2539
+ const kind = kinds[i] ?? "flow";
2540
+ if (kind === "blank") {
2541
+ pendingBlank = true;
2542
+ continue;
2543
+ }
2544
+ if (kind === "flow") {
2545
+ const indent = (text.match(/^ */) ?? [""])[0];
2546
+ if (buf !== null && !pendingBlank && indent === bufIndent) {
2547
+ buf += ` ${text.slice(indent.length)}`;
2548
+ } else {
2549
+ flush();
2550
+ if (pendingBlank && res.length > 0) res.push("");
2551
+ buf = text;
2552
+ bufIndent = indent;
2553
+ }
2554
+ } else {
2555
+ flush();
2556
+ if (pendingBlank && res.length > 0) res.push("");
2557
+ res.push(text);
2558
+ }
2559
+ pendingBlank = false;
2560
+ }
2561
+ flush();
2562
+ while (res.length > 0 && (res[0] ?? "").trim() === "") res.shift();
2563
+ while (res.length > 0 && (res[res.length - 1] ?? "").trim() === "") res.pop();
2564
+ return res;
2565
+ }
2566
+ var PromptDoc = class _PromptDoc {
2567
+ lines;
2568
+ constructor(lines) {
2569
+ this.lines = lines;
2570
+ }
2571
+ static parse(text) {
2572
+ const raw = text.replace(/\r\n/g, "\n").split("\n");
2573
+ if (raw.length > 1 && raw[raw.length - 1] === "") raw.pop();
2574
+ const kinds = classifyLines(raw);
2575
+ const lines = raw.map(
2576
+ (t, i) => ({
2577
+ id: `L${i + 1}`,
2578
+ n: i + 1,
2579
+ text: t,
2580
+ enabled: true,
2581
+ kind: kinds[i] ?? "content"
2582
+ })
2583
+ );
2584
+ return new _PromptDoc(lines);
2585
+ }
2586
+ /** Content lines — the candidates for ablation (blank and comment lines are excluded). */
2587
+ contentLines() {
2588
+ return this.lines.filter((l) => l.kind === "content");
2589
+ }
2590
+ byId(id) {
2591
+ return this.lines.find((l) => l.id === id);
2592
+ }
2593
+ // Enabled, non-comment lines joined back to a prompt string. Whole-line comments are dropped; trailing
2594
+ // `//` inline comments are stripped (but left intact inside fenced code). A declaration (`Name IS value`)
2595
+ // BOTH registers `Name` (so {Name} places its value, and the consistency checker reasons over it) AND
2596
+ // renders as the instruction it is — binding and use are the same statement, not a hidden definition. Only
2597
+ // IMPORT lines and comments are stripped; everything else reaches the model.
2598
+ render() {
2599
+ const texts = this.lines.map((l) => l.text);
2600
+ const mask = fenceMask(texts);
2601
+ const toolMask = toolBlockMask(texts);
2602
+ const toolDescs = /* @__PURE__ */ new Map();
2603
+ for (const t of parseTools(texts).tools) toolDescs.set(t.name.toLowerCase(), t.description);
2604
+ const importedToolKeys = toolsFromLines(texts);
2605
+ const decls = /* @__PURE__ */ new Map();
2606
+ this.lines.forEach((l, i) => {
2607
+ if (!l.enabled || l.kind === "comment" || mask[i] || toolMask[i]) return;
2608
+ const d = findDeclaration(stripAnnotation(stripInlineComment(l.text)));
2609
+ if (d && !d.negated) decls.set(d.name.toLowerCase(), d.value);
2610
+ });
2611
+ const active = this.lines.filter((l) => l.enabled && l.kind !== "comment").map((l) => l.text);
2612
+ for (const [key, obj] of collectObjects(active)) {
2613
+ for (const [path, pv] of obj.properties) {
2614
+ const value = pv.values.join(", ");
2615
+ decls.set(`${key}.${path}`, value);
2616
+ for (const alias of obj.aliases) decls.set(`${alias.toLowerCase()}.${path}`, value);
2617
+ }
2618
+ }
2619
+ const out = [];
2620
+ const kinds = [];
2621
+ const testLines = testBlockLines(texts);
2622
+ const exampleLines = exampleBlockLines(texts);
2623
+ const exampleStart = new Map(parseExamples(texts).filter((e) => e.error === null).map((e) => [e.headerLine, e]));
2624
+ const cfgMask = configSectionMask(texts);
2625
+ const switchRw = switchProseRewrite(texts);
2626
+ const QUIET_WRAPPER = /<\/?\?[a-z][a-z0-9_]*(?:\s[^>]*)?>/gi;
2627
+ const litMask = literalSectionMask(texts);
2628
+ const defMask = defListMask(texts);
2629
+ const defStart = new Map(defEntries(texts).map((e) => [e.line, e]));
2630
+ const LITERAL_SIGIL = /<(\/?)"([a-z][a-z0-9_]*)"(?=[\s>])/gi;
2631
+ this.lines.forEach((l, i) => {
2632
+ if (!l.enabled || l.kind === "comment" || toolMask[i] || cfgMask[i]) return;
2633
+ if (testLines.has(i)) return;
2634
+ const ex = exampleStart.get(i);
2635
+ if (ex) {
2636
+ const block = renderExample(ex);
2637
+ out.push(...block);
2638
+ for (let b = 0; b < block.length; b += 1) kinds.push("keep");
2639
+ return;
2640
+ }
2641
+ if (exampleLines.has(i)) return;
2642
+ if (defMask[i]) {
2643
+ const eDef = defStart.get(i);
2644
+ if (eDef) {
2645
+ const block = renderDefBlock(texts, eDef);
2646
+ out.push(...block);
2647
+ for (let b = 0; b < block.length; b += 1) kinds.push("keep");
2648
+ }
2649
+ return;
2650
+ }
2651
+ if (mask[i]) {
2652
+ out.push(l.text);
2653
+ kinds.push("keep");
2654
+ return;
2655
+ }
2656
+ if (litMask[i]) {
2657
+ out.push(l.text);
2658
+ kinds.push("keep");
2659
+ return;
2660
+ }
2661
+ const swr = switchRw.get(i);
2662
+ if (swr === null) return;
2663
+ if (isAnnotationLine(swr ?? l.text)) return;
2664
+ const stripped = stripAnnotation(stripInlineComment(swr ?? l.text));
2665
+ if (IMPORT_RE.test(stripped) || IMPORT_FILE_RE.test(stripped) || REQUIRE_RE.test(stripped) || CONFIG_RE.test(stripped) || DEFINE_RE.test(stripped)) return;
2666
+ const unwrapped = stripped.replace(QUIET_WRAPPER, "");
2667
+ const hadWrapper = unwrapped !== stripped;
2668
+ const body = hadWrapper ? unwrapped.trim() : stripped;
2669
+ if (hadWrapper && body === "") return;
2670
+ const emitted = expandToolRefs(expandRefs(stripQuotes(expandInlineToProse(body)), decls), toolDescs, importedToolKeys).replace(LITERAL_SIGIL, "<$1$2");
2671
+ out.push(emitted);
2672
+ kinds.push(classifyCompiledLine(emitted));
2673
+ });
2674
+ return joinCompiledLines(out, kinds).join("\n");
2675
+ }
2676
+ /** A copy of this doc with the given line disabled (leave-one-out ablation). */
2677
+ without(id) {
2678
+ return new _PromptDoc(this.lines.map((l) => l.id === id ? { ...l, enabled: false } : { ...l }));
2679
+ }
2680
+ toData() {
2681
+ return { lines: this.lines.map((l) => ({ ...l })) };
2682
+ }
2683
+ };
2684
+
2685
+ // src/core/xml.ts
2686
+ function tagName(heading) {
2687
+ return heading.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "section";
2688
+ }
2689
+ function compileToXml(systemContent, instructionsContent) {
2690
+ const parts = [];
2691
+ const sys = PromptDoc.parse(stripIndent(systemContent)).render();
2692
+ if (sys) parts.push(sys);
2693
+ const instr = PromptDoc.parse(stripIndent(instructionsContent)).render();
2694
+ if (instr) parts.push(instr);
2695
+ return parts.join("\n\n");
2696
+ }
2697
+
2698
+ // src/core/config-schema.ts
2699
+ var BUILTIN_CONFIG_SCHEMA = {
2700
+ model: {
2701
+ detail: "the target model \u2014 id on the header, settings as params",
2702
+ placeholder: "claude-opus-4",
2703
+ params: {
2704
+ via: { detail: "the $SERVICE that carries the request", type: { kind: "service" }, required: true, placeholder: "service_name" },
2705
+ max_tokens: { detail: "the output token cap", type: { kind: "prim", prim: "number" }, placeholder: "8192" },
2706
+ temperature: { detail: "sampling temperature (0.0\u20132.0)", type: { kind: "prim", prim: "number" }, placeholder: "0.3" }
2707
+ }
2708
+ },
2709
+ modality: { detail: "the interaction channel", values: ["voice", "chat"] },
2710
+ language: { detail: "a language code", placeholder: "en" },
2711
+ // A SIMPLE kind: extra addressee nouns are a flat list on the header/domain form, no params needed.
2712
+ addressee: { detail: 'extra subjects that mean "you" (the addressee model, SPEC \xA77)', placeholder: "one of assistant, support_agent" }
2713
+ };
2714
+ function renderParamType(t) {
2715
+ if (t.kind === "service") return "a $SERVICE name";
2716
+ if (t.kind === "object") return "a nested block";
2717
+ return renderTypeExpr(t);
2718
+ }
2719
+ function placeholderFor(t) {
2720
+ if (t.kind === "service") return "service_name";
2721
+ if (t.kind === "prim") return t.prim === "boolean" ? "true" : t.prim === "string" ? "text" : "0";
2722
+ if (t.kind === "oneOf" || t.kind === "anyOf") return t.members[0];
2723
+ if (t.kind === "list") return `${t.prim}, \u2026`;
2724
+ return void 0;
2725
+ }
2726
+ function paramFromType(text) {
2727
+ let t = text.trim();
2728
+ let required;
2729
+ const marker = t.match(/^(required|optional)\s+(.+)$/i);
2730
+ if (marker) {
2731
+ required = marker[1].toLowerCase() === "required";
2732
+ t = marker[2].trim();
2733
+ }
2734
+ let type;
2735
+ if (/^service$/i.test(t)) {
2736
+ type = { kind: "service" };
2737
+ } else {
2738
+ const parsed = parseTypeExpr(t);
2739
+ type = parsed?.ok ? parsed.expr : { kind: "prim", prim: "string" };
2740
+ }
2741
+ return { detail: renderParamType(type), type, required, placeholder: placeholderFor(type) };
2742
+ }
2743
+ function paramsFromBody(leaves, objects) {
2744
+ const out = {};
2745
+ for (const [name, typeText] of leaves) out[name] = paramFromType(typeText);
2746
+ for (const [name, obj] of objects ?? /* @__PURE__ */ new Map()) {
2747
+ out[name] = { detail: "a nested block", type: { kind: "object", params: paramsFromBody(obj.params, obj.objects) } };
2748
+ }
2749
+ return out;
2750
+ }
2751
+ function parseGlishConfigSchema(glishSource) {
2752
+ const out = {};
2753
+ for (const block of parseConfigBlocks(glishSource.split("\n"))) {
2754
+ const key = { detail: "" };
2755
+ if (block.value) {
2756
+ const parsed = parseTypeExpr(block.value);
2757
+ if (parsed?.ok && (parsed.expr.kind === "oneOf" || parsed.expr.kind === "anyOf")) {
2758
+ key.values = parsed.expr.members;
2759
+ key.detail = `one of: ${parsed.expr.members.join(", ")}`;
2760
+ } else {
2761
+ key.placeholder = block.value;
2762
+ }
2763
+ }
2764
+ const params = paramsFromBody(block.params, block.objects);
2765
+ if (Object.keys(params).length > 0) key.params = params;
2766
+ out[block.kind] = key;
2767
+ }
2768
+ return out;
2769
+ }
2770
+ function resolveConfigSchema(glishSource) {
2771
+ if (!glishSource) return BUILTIN_CONFIG_SCHEMA;
2772
+ const out = { ...BUILTIN_CONFIG_SCHEMA };
2773
+ for (const [kind, gk] of Object.entries(parseGlishConfigSchema(glishSource))) {
2774
+ const base = out[kind];
2775
+ out[kind] = base ? {
2776
+ detail: gk.detail || base.detail,
2777
+ values: gk.values ?? base.values,
2778
+ placeholder: gk.placeholder ?? base.placeholder,
2779
+ ...base.params || gk.params ? { params: { ...base.params, ...gk.params } } : {}
2780
+ } : { ...gk, detail: gk.detail || "declared in glish.tgc" };
2781
+ }
2782
+ return out;
2783
+ }
2784
+ var CONFIG_HEADER_RE = /^\s*\$CONFIG\s+([a-z][\w-]*)(?:\s+(\S.*?))?\s*$/i;
2785
+ var CONFIG_PARAM_RE = /^\s*-\s+([a-z][a-z0-9_]*)::\s*(.*?)\s*$/i;
2786
+ var indentWidth = (line) => (line.match(/^[ \t]*/) ?? [""])[0].length;
2787
+ function parseParamBody(rawLines, start, parentIndent) {
2788
+ const params = /* @__PURE__ */ new Map();
2789
+ const objects = /* @__PURE__ */ new Map();
2790
+ let j = start;
2791
+ while (j < rawLines.length) {
2792
+ const line = rawLines[j] ?? "";
2793
+ if (line.trim() === "") {
2794
+ j += 1;
2795
+ continue;
2796
+ }
2797
+ if (indentWidth(line) <= parentIndent) break;
2798
+ const pm = line.match(CONFIG_PARAM_RE);
2799
+ if (!pm) {
2800
+ j += 1;
2801
+ continue;
2802
+ }
2803
+ const name = (pm[1] ?? "").toLowerCase();
2804
+ const value = (pm[2] ?? "").trim();
2805
+ let nextDeeper = false;
2806
+ for (let k = j + 1; k < rawLines.length; k += 1) {
2807
+ const nl = rawLines[k] ?? "";
2808
+ if (nl.trim() === "") continue;
2809
+ nextDeeper = indentWidth(nl) > indentWidth(line);
2810
+ break;
2811
+ }
2812
+ if (value === "" && nextDeeper) {
2813
+ const sub = parseParamBody(rawLines, j + 1, indentWidth(line));
2814
+ objects.set(name, { params: sub.params, ...sub.objects.size ? { objects: sub.objects } : {}, line: j });
2815
+ j = sub.end;
2816
+ } else {
2817
+ params.set(name, value);
2818
+ j += 1;
2819
+ }
2820
+ }
2821
+ return { params, objects, end: j };
2822
+ }
2823
+ function parseConfigBlocks(rawLines) {
2824
+ const blocks = [];
2825
+ let inFence = false;
2826
+ for (let i = 0; i < rawLines.length; i += 1) {
2827
+ const t = (rawLines[i] ?? "").trim();
2828
+ if (t.startsWith("```") || t.startsWith("~~~")) {
2829
+ inFence = !inFence;
2830
+ continue;
2831
+ }
2832
+ const h = inFence ? null : (rawLines[i] ?? "").match(CONFIG_HEADER_RE);
2833
+ if (!h) continue;
2834
+ const body = parseParamBody(rawLines, i + 1, indentWidth(rawLines[i] ?? ""));
2835
+ blocks.push({ kind: (h[1] ?? "").toLowerCase(), value: (h[2] ?? "").trim(), params: body.params, ...body.objects.size ? { objects: body.objects } : {}, line: i });
2836
+ i = body.end - 1;
2837
+ }
2838
+ return blocks;
2839
+ }
2840
+ function refineViolation(t, value) {
2841
+ if (t.kind !== "prim" || !t.refine) return null;
2842
+ const r = t.refine;
2843
+ if (t.prim === "string") {
2844
+ if (r.pattern !== void 0 && !new RegExp(r.pattern).test(value)) return `must match /${r.pattern}/`;
2845
+ if (r.minLen !== void 0 && value.length < r.minLen) return `must be at least ${r.minLen} characters`;
2846
+ if (r.maxLen !== void 0 && value.length > r.maxLen) return `must be at most ${r.maxLen} characters`;
2847
+ return null;
2848
+ }
2849
+ const n = Number(value);
2850
+ if (!Number.isFinite(n)) return null;
2851
+ if (r.min !== void 0 && n < r.min) return `must be at least ${r.min}`;
2852
+ if (r.max !== void 0 && n > r.max) return `must be at most ${r.max}`;
2853
+ return null;
2854
+ }
2855
+ function configParamDiagnostics(rawLines, schema, serviceNames) {
2856
+ const out = [];
2857
+ const range = (line) => {
2858
+ const raw = rawLines[line] ?? "";
2859
+ return { line, start: raw.length - raw.trimStart().length, end: raw.trimEnd().length };
2860
+ };
2861
+ const indentOf = (line) => ((rawLines[line] ?? "").match(/^\s*/)?.[0] ?? "").length;
2862
+ const paramLine = (headerLine, name) => {
2863
+ const re = new RegExp(`^\\s*-\\s+${name}::`, "i");
2864
+ for (let j = headerLine + 1; j < rawLines.length; j += 1) {
2865
+ const l = rawLines[j] ?? "";
2866
+ if (l.trim() === "") continue;
2867
+ if (indentOf(j) <= indentOf(headerLine)) break;
2868
+ if (re.test(l)) return j;
2869
+ }
2870
+ return headerLine;
2871
+ };
2872
+ const checkBody = (schemaParams, leaves, objects, kind, headerLine) => {
2873
+ for (const [name, p] of Object.entries(schemaParams)) {
2874
+ const present = p.type.kind === "object" ? !!objects?.has(name) : leaves.has(name);
2875
+ if (p.required && !present) {
2876
+ out.push({ ...range(headerLine), severity: "warn", code: "config/missing-param", message: `$CONFIG ${kind} is missing the required "${name}" \u2014 add "- ${name}:: \u2026" beneath it.` });
2877
+ }
2878
+ }
2879
+ leaves.forEach((value, name) => {
2880
+ const p = schemaParams[name];
2881
+ const ln = paramLine(headerLine, name);
2882
+ if (p === void 0) {
2883
+ out.push({ ...range(ln), severity: "info", code: "config/unknown-param", message: `"${name}" isn't a known param of $CONFIG ${kind}.` });
2884
+ } else if (p.type.kind === "object") {
2885
+ out.push({ ...range(ln), severity: "info", code: "config/unknown-param", message: `$CONFIG ${kind} \u2192 ${name} is a nested block \u2014 give it indented "- \u2026::" rows, not a value.` });
2886
+ } else if (value !== "") {
2887
+ const t = p.type;
2888
+ const num = Number(value);
2889
+ if (t.kind === "service" && !serviceNames.has(value)) {
2890
+ out.push({ ...range(ln), severity: "error", code: "config/dangling-service", message: `$CONFIG ${kind} \u2192 ${name} names "${value}", but no "$SERVICE ${value}" is defined.` });
2891
+ } else if (t.kind === "prim" && (t.prim === "number" || t.prim === "integer") && !Number.isFinite(num)) {
2892
+ out.push({ ...range(ln), severity: "warn", code: "config/bad-number", message: `$CONFIG ${kind} \u2192 ${name} should be a number \u2014 got "${value}".` });
2893
+ } else if (t.kind === "prim" && t.prim === "integer" && !Number.isInteger(num)) {
2894
+ out.push({ ...range(ln), severity: "warn", code: "config/bad-number", message: `$CONFIG ${kind} \u2192 ${name} should be a whole number \u2014 got "${value}".` });
2895
+ } else {
2896
+ const v = refineViolation(t, value);
2897
+ if (v) out.push({ ...range(ln), severity: "warn", code: "config/out-of-range", message: `$CONFIG ${kind} \u2192 ${name} ${v} \u2014 got "${value}".` });
2898
+ }
2899
+ }
2900
+ });
2901
+ objects?.forEach((obj, name) => {
2902
+ const p = schemaParams[name];
2903
+ if (p === void 0) {
2904
+ out.push({ ...range(obj.line), severity: "info", code: "config/unknown-param", message: `"${name}" isn't a known param of $CONFIG ${kind}.` });
2905
+ } else if (p.type.kind !== "object") {
2906
+ out.push({ ...range(obj.line), severity: "info", code: "config/unknown-param", message: `$CONFIG ${kind} \u2192 ${name} takes a value, not a nested block.` });
2907
+ } else {
2908
+ checkBody(p.type.params, obj.params, obj.objects, `${kind} \u2192 ${name}`, obj.line);
2909
+ }
2910
+ });
2911
+ };
2912
+ for (const block of parseConfigBlocks(rawLines)) {
2913
+ const params = schema[block.kind]?.params;
2914
+ if (params) checkBody(params, block.params, block.objects, block.kind, block.line);
2915
+ }
2916
+ return out;
2917
+ }
2918
+ var CONFIG_RULE_RE = /^\s*\$REQUIRE\s+([a-z][\w-]*)\s+when\s+([a-z][\w-]*)\s+is\s+(\S.*?)\s*$/i;
2919
+ function parseConfigRules(glishSource) {
2920
+ const rules = [];
2921
+ glishSource.split("\n").forEach((raw, line) => {
2922
+ const m = raw.match(CONFIG_RULE_RE);
2923
+ if (m) rules.push({ target: (m[1] ?? "").toLowerCase(), whenKind: (m[2] ?? "").toLowerCase(), whenValue: (m[3] ?? "").trim(), line });
2924
+ });
2925
+ return rules;
2926
+ }
2927
+ function configRuleDiagnostics(rawLines, rules) {
2928
+ if (rules.length === 0) return [];
2929
+ const byKind = new Map(parseConfigBlocks(rawLines).map((b) => [b.kind, b]));
2930
+ const out = [];
2931
+ for (const rule of rules) {
2932
+ const cond = byKind.get(rule.whenKind);
2933
+ if (cond && cond.value === rule.whenValue && !byKind.has(rule.target)) {
2934
+ const raw = rawLines[cond.line] ?? "";
2935
+ out.push({ line: cond.line, start: raw.length - raw.trimStart().length, end: raw.trimEnd().length, severity: "warn", code: "config/missing-conditional", message: `$CONFIG ${rule.whenKind} is "${rule.whenValue}", so $CONFIG ${rule.target} is required \u2014 add it.` });
2936
+ }
2937
+ }
2938
+ return out;
2939
+ }
2940
+
2941
+ // src/core/models.generated.ts
2942
+ var MODELS_GENERATED_AT = "2026-07-01";
2943
+ var KNOWN_MODELS = [
2944
+ // Anthropic — https://api.anthropic.com/v1/models
2945
+ // Current line-up, confirmed against the live endpoint 2026-07-09 (a full refresh would drop the
2946
+ // committed OpenAI/Google entries when those keys are absent, so these three merged surgically).
2947
+ { id: "claude-opus-4-8", vendor: "anthropic" },
2948
+ { id: "claude-sonnet-4-6", vendor: "anthropic" },
2949
+ { id: "claude-haiku-4-5-20251001", vendor: "anthropic", aliases: ["claude-haiku-4-5"] },
2950
+ { id: "claude-opus-4-1-20250805", vendor: "anthropic", aliases: ["claude-opus-4-1"] },
2951
+ { id: "claude-opus-4-20250514", vendor: "anthropic", aliases: ["claude-opus-4-0"] },
2952
+ { id: "claude-sonnet-4-20250514", vendor: "anthropic", aliases: ["claude-sonnet-4-0"] },
2953
+ { id: "claude-3-7-sonnet-20250219", vendor: "anthropic", aliases: ["claude-3-7-sonnet-latest"] },
2954
+ { id: "claude-3-5-sonnet-20241022", vendor: "anthropic", aliases: ["claude-3-5-sonnet-latest"] },
2955
+ { id: "claude-3-5-haiku-20241022", vendor: "anthropic", aliases: ["claude-3-5-haiku-latest"] },
2956
+ { id: "claude-3-haiku-20240307", vendor: "anthropic" },
2957
+ // OpenAI — https://api.openai.com/v1/models (chat/reasoning models only)
2958
+ { id: "gpt-4o", vendor: "openai" },
2959
+ { id: "gpt-4o-mini", vendor: "openai" },
2960
+ { id: "gpt-4.1", vendor: "openai" },
2961
+ { id: "gpt-4.1-mini", vendor: "openai" },
2962
+ { id: "o3", vendor: "openai" },
2963
+ { id: "o4-mini", vendor: "openai" },
2964
+ // Google — https://generativelanguage.googleapis.com/v1beta/models
2965
+ { id: "gemini-2.5-pro", vendor: "google" },
2966
+ { id: "gemini-2.5-flash", vendor: "google" },
2967
+ { id: "gemini-2.0-flash", vendor: "google" }
2968
+ ];
2969
+
2970
+ // src/core/models.ts
2971
+ var norm2 = (id) => id.trim().toLowerCase();
2972
+ var stem = (id) => norm2(id).replace(/-\d{4,}$/, "");
2973
+ var CANDIDATES = /* @__PURE__ */ new Map();
2974
+ var KNOWN = /* @__PURE__ */ new Set();
2975
+ for (const m of [...KNOWN_MODELS].sort((a, b) => a.id.length - b.id.length)) {
2976
+ const id = norm2(m.id);
2977
+ KNOWN.add(id);
2978
+ if (!CANDIDATES.has(id)) CANDIDATES.set(id, m.id);
2979
+ const s = stem(m.id);
2980
+ if (!CANDIDATES.has(s)) CANDIDATES.set(s, m.id);
2981
+ for (const a of m.aliases ?? []) {
2982
+ KNOWN.add(norm2(a));
2983
+ if (!CANDIDATES.has(norm2(a))) CANDIDATES.set(norm2(a), m.id);
2984
+ }
2985
+ }
2986
+ var CANONICAL_IDS = [...new Set(KNOWN_MODELS.map((m) => m.id))];
2987
+ var FAMILIES = ["claude-", "gpt-", "gemini-", "o1", "o3", "o4", "text-", "chatgpt-"];
2988
+ function damerau(a, b) {
2989
+ const al = a.length;
2990
+ const bl = b.length;
2991
+ if (al === 0) return bl;
2992
+ if (bl === 0) return al;
2993
+ const d = Array.from({ length: al + 1 }, () => new Array(bl + 1).fill(0));
2994
+ for (let i = 0; i <= al; i += 1) d[i][0] = i;
2995
+ for (let j = 0; j <= bl; j += 1) d[0][j] = j;
2996
+ for (let i = 1; i <= al; i += 1) {
2997
+ for (let j = 1; j <= bl; j += 1) {
2998
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2999
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
3000
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
3001
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
3002
+ }
3003
+ }
3004
+ }
3005
+ return d[al][bl];
3006
+ }
3007
+ function classifyModel(id) {
3008
+ const n = norm2(id);
3009
+ if (KNOWN.has(n)) return { kind: "known" };
3010
+ const dated = /-\d{4,}$/.test(n);
3011
+ if (!dated) {
3012
+ const prefixed = CANONICAL_IDS.filter((k) => norm2(k).startsWith(`${n}-`)).sort((a, b) => a.length - b.length);
3013
+ if (prefixed.length > 0) return { kind: "incomplete", suggest: prefixed[0] };
3014
+ let best = null;
3015
+ for (const [cand, canonical] of CANDIDATES) {
3016
+ const dist = damerau(n, cand);
3017
+ if (!best || dist < best.dist) best = { suggest: canonical, dist };
3018
+ }
3019
+ if (best && best.dist <= Math.max(1, Math.floor(n.length * 0.2))) return { kind: "typo", suggest: best.suggest };
3020
+ }
3021
+ if (FAMILIES.some((f) => n.startsWith(f))) return { kind: "maybe-newer" };
3022
+ return { kind: "unknown" };
3023
+ }
3024
+ function checkModel(rawLines) {
3025
+ const out = [];
3026
+ for (let i = 0; i < rawLines.length; i += 1) {
3027
+ const raw = rawLines[i] ?? "";
3028
+ const m = raw.match(CONFIG_RE);
3029
+ if (!m || (m[1] ?? "").toLowerCase() !== "model") continue;
3030
+ const value = (m[2] ?? "").split(",")[0].trim();
3031
+ if (value === "") continue;
3032
+ const start = raw.length - raw.trimStart().length;
3033
+ const end = raw.trimEnd().length;
3034
+ const v = classifyModel(value);
3035
+ if (v.kind === "known") continue;
3036
+ if (v.kind === "maybe-newer") {
3037
+ out.push({
3038
+ line: i,
3039
+ start,
3040
+ end,
3041
+ severity: "info",
3042
+ message: `"${value}" isn't in the known-model list (as of ${MODELS_GENERATED_AT}). If it's newer than the list, ignore this \u2014 the list refreshes from the vendors.`
3043
+ });
3044
+ continue;
3045
+ }
3046
+ const suggest = v.kind === "incomplete" || v.kind === "typo" ? ` Did you mean "${v.suggest}"?` : "";
3047
+ out.push({
3048
+ line: i,
3049
+ start,
3050
+ end,
3051
+ severity: "warn",
3052
+ message: `Unknown model "${value}".${suggest} $CONFIG model isn't validated at runtime, so a wrong id fails only when the agent runs.`
3053
+ });
3054
+ }
3055
+ return out;
3056
+ }
3057
+
3058
+ // src/core/structure.ts
3059
+ var STEP = /^(\d+(?:\.\d+)*)[.)]\s+(.*)$/;
3060
+ var ANCHOR_DECL = /^#([a-z][a-z0-9_]*)\s+(.*)$/i;
3061
+ function splitAnchor(text) {
3062
+ const m = text.match(ANCHOR_DECL);
3063
+ return m ? { anchor: (m[1] ?? "").toLowerCase(), text: m[2] ?? "" } : { anchor: null, text };
3064
+ }
3065
+ var SECTION_LEAD = new RegExp(`^\\s*<(\\/?)(${TAG_NAME_SRC})(?:\\s[^>]*)?>`, "i");
3066
+ var SECTION_CLOSE_TAIL = new RegExp(`<\\/(${TAG_NAME_SRC})>\\s*$`, "i");
3067
+ function matchSectionTag(raw) {
3068
+ const lead = raw.match(SECTION_LEAD);
3069
+ if (lead) {
3070
+ const name = (lead[2] ?? "").toLowerCase();
3071
+ if (lead[1] === "/") return { open: null, close: name, inline: false };
3072
+ const closeM2 = raw.slice((lead.index ?? 0) + lead[0].length).match(SECTION_CLOSE_TAIL);
3073
+ return closeM2 ? { open: name, close: (closeM2[1] ?? "").toLowerCase(), inline: true } : { open: name, close: null, inline: false };
3074
+ }
3075
+ const closeM = raw.match(SECTION_CLOSE_TAIL);
3076
+ return closeM ? { open: null, close: (closeM[1] ?? "").toLowerCase(), inline: false } : null;
3077
+ }
3078
+ var HEADING = /^\s*(#{1,6})\s+(.+?)\s*$/;
3079
+ var VARREF = /@\{([a-z][a-z0-9_]*)\}/g;
3080
+ function parseWhenAttr(raw) {
3081
+ const m = raw.match(/\bwhen=/i);
3082
+ if (!m || m.index === void 0) return null;
3083
+ const col = m.index + m[0].length;
3084
+ const gt = raw.indexOf(">", col);
3085
+ return gt < 0 ? null : { text: raw.slice(col, gt).trim(), col };
3086
+ }
3087
+ function collectVarUses(masked, line, vars) {
3088
+ VARREF.lastIndex = 0;
3089
+ let m;
3090
+ while ((m = VARREF.exec(masked)) !== null) {
3091
+ const base = (m[1] ?? "").toLowerCase();
3092
+ const sym = vars.get(base) ?? { base, notations: [] };
3093
+ sym.notations.push({ form: "curly", line, start: m.index, end: m.index + m[0].length });
3094
+ vars.set(base, sym);
3095
+ }
3096
+ }
3097
+ var INPUT_IDENT = /^[a-z][a-z0-9_]*$/i;
3098
+ function parseInputType(typeStr) {
3099
+ const parsed = parseTypeExpr(typeStr);
3100
+ if (parsed?.ok) {
3101
+ const e = parsed.expr;
3102
+ if (e.kind === "prim") {
3103
+ return { type: e.prim === "boolean" ? { kind: "bool" } : e.prim === "string" ? { kind: "string" } : { kind: "number" }, error: null };
3104
+ }
3105
+ if (e.kind === "oneOf") {
3106
+ const members = e.members.map((m) => m.toLowerCase());
3107
+ const bad = members.find((v) => !INPUT_IDENT.test(v));
3108
+ if (bad !== void 0) return { type: null, error: `Set member "${bad}" is not a valid identifier (letters, digits, _ \u2014 starting with a letter).` };
3109
+ return { type: { kind: "enum", members }, error: null };
3110
+ }
3111
+ return { type: null, error: `A variable's domain is ONE value \u2014 a primitive or a "one of a, b" set, never a list ("${typeStr.trim()}").` };
3112
+ }
3113
+ if (parsed && !parsed.ok) {
3114
+ return { type: null, error: `${parsed.reason}${parsed.rewrite !== void 0 ? ` Replace the type with: "${parsed.rewrite}".` : ""}` };
3115
+ }
3116
+ const legacy = translateLegacyTypeHead(typeStr);
3117
+ if (legacy !== null) {
3118
+ return { type: null, error: `Domains are English \u2014 "one of a, b" for a closed set \u2014 never "|" pipes. Replace the type with: "${legacy}".` };
3119
+ }
3120
+ return { type: null, error: `"${typeStr.trim()}" is not a domain. The forms: boolean, number, integer, string, or "one of a, b".` };
3121
+ }
3122
+ function parseTypedInput(payload) {
3123
+ const ci = payload.indexOf(":");
3124
+ const name = payload.slice(0, ci).trim();
3125
+ const typeStr = payload.slice(ci + 1).trim();
3126
+ if (!INPUT_IDENT.test(name)) return { name, type: null, error: `"${name || "(empty)"}" is not a valid input name.` };
3127
+ if (typeStr === "") return { name, type: null, error: `Input "${name}" has no domain after ":" \u2014 write ": boolean", ": number", ": string", or ": one of a, b".` };
3128
+ const { type, error } = parseInputType(typeStr);
3129
+ return { name, type, error: error === null ? null : `Input "${name}": ${error}` };
3130
+ }
3131
+ function looseContentLines(rawLines) {
3132
+ const kinds = classifyLines(rawLines);
3133
+ const toolMask = toolBlockMask(rawLines);
3134
+ const exMask = examplesMask(rawLines);
3135
+ const cover = [];
3136
+ const loose = [];
3137
+ let sawSection = false;
3138
+ let inFence = false;
3139
+ rawLines.forEach((raw, i) => {
3140
+ const t = raw.trim();
3141
+ if (t.startsWith("```") || t.startsWith("~~~")) {
3142
+ inFence = !inFence;
3143
+ return;
3144
+ }
3145
+ if (inFence || kinds[i] !== "content" || toolMask[i] || exMask[i]) return;
3146
+ if (TRANSCLUSION_LINE_RE.test(raw)) return;
3147
+ const hm = raw.match(HEADING);
3148
+ if (hm) {
3149
+ const level = (hm[1] ?? "").length;
3150
+ while (cover.length > 0) {
3151
+ const top = cover[cover.length - 1];
3152
+ if (top.kind === "heading" && top.level >= level) cover.pop();
3153
+ else break;
3154
+ }
3155
+ cover.push({ kind: "heading", level });
3156
+ sawSection = true;
3157
+ return;
3158
+ }
3159
+ const st = matchSectionTag(raw);
3160
+ if (st) {
3161
+ if (st.open) {
3162
+ while (cover.length > 0 && cover[cover.length - 1].kind === "heading") cover.pop();
3163
+ cover.push({ kind: "tag", name: st.open });
3164
+ sawSection = true;
3165
+ }
3166
+ if (st.close) {
3167
+ for (let s = cover.length - 1; s >= 0; s -= 1) {
3168
+ const c = cover[s];
3169
+ if (c.kind === "tag" && c.name === st.close) {
3170
+ cover.length = s;
3171
+ break;
3172
+ }
3173
+ }
3174
+ }
3175
+ const leadClose = raw.match(/^\s*<\/(?:\$?[a-z][a-z0-9_]*)>/i);
3176
+ if (leadClose && sawSection && cover.length === 0 && raw.slice(leadClose[0].length).trim() !== "") {
3177
+ loose.push({ line: i, start: leadClose[0].length });
3178
+ }
3179
+ return;
3180
+ }
3181
+ if (t.startsWith("$")) return;
3182
+ if (sawSection && cover.length === 0) loose.push({ line: i, start: raw.length - raw.trimStart().length });
3183
+ });
3184
+ return loose;
3185
+ }
3186
+ function headingsOutsideSections(rawLines) {
3187
+ const doc = buildStructure(rawLines);
3188
+ const xml = doc.sections.filter((s) => s.syntax === "xml" && s.control !== true);
3189
+ if (xml.length === 0) return [];
3190
+ const fenced = /* @__PURE__ */ new Set();
3191
+ let inFence = false;
3192
+ rawLines.forEach((raw, i) => {
3193
+ const t = raw.trim();
3194
+ if (t.startsWith("```") || t.startsWith("~~~")) {
3195
+ fenced.add(i);
3196
+ inFence = !inFence;
3197
+ return;
3198
+ }
3199
+ if (inFence) fenced.add(i);
3200
+ });
3201
+ const out = [];
3202
+ for (const h of doc.sections) {
3203
+ if (h.syntax !== "heading" || fenced.has(h.startLine)) continue;
3204
+ const inside = xml.some((x) => h.startLine >= x.startLine && h.startLine < x.endLine);
3205
+ if (inside) continue;
3206
+ const raw = rawLines[h.startLine] ?? "";
3207
+ out.push({ line: h.startLine, start: raw.length - raw.trimStart().length });
3208
+ }
3209
+ return out;
3210
+ }
3211
+ function buildStructure(rawLines) {
3212
+ const kinds = classifyLines(rawLines);
3213
+ const steps = [];
3214
+ const sections = [];
3215
+ const variables = /* @__PURE__ */ new Map();
3216
+ const imports = [];
3217
+ const fileImports = [];
3218
+ const importLines = /* @__PURE__ */ new Set();
3219
+ const config = /* @__PURE__ */ new Map();
3220
+ const configLines = /* @__PURE__ */ new Set();
3221
+ const inputTypes = /* @__PURE__ */ new Map();
3222
+ const defines = /* @__PURE__ */ new Map();
3223
+ const defineLines = /* @__PURE__ */ new Set();
3224
+ const xmlStack = [];
3225
+ const strayCloses = [];
3226
+ const headingStack = [];
3227
+ const toolMask = toolBlockMask(rawLines);
3228
+ const exMask = examplesMask(rawLines);
3229
+ rawLines.forEach((raw, i) => {
3230
+ if (kinds[i] === "comment" || toolMask[i] || exMask[i]) return;
3231
+ collectVarUses(maskQuotes(raw), i, variables);
3232
+ const fileImp = raw.match(IMPORT_FILE_RE);
3233
+ if (fileImp) {
3234
+ importLines.add(i);
3235
+ fileImports.push({ alias: fileImp[2] ?? "", key: (fileImp[2] ?? "").toLowerCase(), path: fileImp[1] ?? "", line: i });
3236
+ return;
3237
+ }
3238
+ const rq = raw.match(REQUIRE_RE);
3239
+ const im = rq ? null : raw.match(IMPORT_RE);
3240
+ if (rq || im) {
3241
+ importLines.add(i);
3242
+ const kind = rq ? "variable" : im[1] === "input" ? "variable" : im[1];
3243
+ const payload = (rq ? rq[1] : im[2]) ?? "";
3244
+ if (kind === "variable" && payload.includes(":")) {
3245
+ const { name, type } = parseTypedInput(payload);
3246
+ if (INPUT_IDENT.test(name)) {
3247
+ imports.push({ kind, name, key: name.toLowerCase(), line: i, ...type ? { type } : {} });
3248
+ if (type) inputTypes.set(name.toLowerCase(), type);
3249
+ }
3250
+ return;
3251
+ }
3252
+ for (const rawName of payload.split(",")) {
3253
+ const name = rawName.trim().replace(/^["']|["']$/g, "").trim();
3254
+ if (name !== "") imports.push({ kind, name, key: name.toLowerCase(), line: i });
3255
+ }
3256
+ return;
3257
+ }
3258
+ const cf = raw.match(CONFIG_RE);
3259
+ if (cf) {
3260
+ configLines.add(i);
3261
+ const key = (cf[1] ?? "").toLowerCase();
3262
+ if (!config.has(key)) config.set(key, (cf[2] ?? "").trim());
3263
+ return;
3264
+ }
3265
+ const df = raw.match(DEFINE_RE);
3266
+ if (df) {
3267
+ defineLines.add(i);
3268
+ const name = (df[1] ?? "").toLowerCase();
3269
+ if (!defines.has(name)) defines.set(name, { expr: (df[2] ?? "").trim(), line: i });
3270
+ return;
3271
+ }
3272
+ if (CONFIG_SECTION_OPEN.test(raw)) {
3273
+ xmlStack.push({ tag: "$config", line: i });
3274
+ return;
3275
+ }
3276
+ if (CONFIG_SECTION_CLOSE.test(raw)) {
3277
+ let matched = false;
3278
+ for (let s = xmlStack.length - 1; s >= 0; s -= 1) {
3279
+ if (xmlStack[s]?.tag === "$config") {
3280
+ const o = xmlStack.splice(s, 1)[0];
3281
+ if (o) sections.push({ tag: "$config", name: "<$config>", syntax: "xml", startLine: o.line, endLine: i + 1, depth: s, control: true });
3282
+ matched = true;
3283
+ break;
3284
+ }
3285
+ }
3286
+ if (!matched) strayCloses.push({ tag: "$config", line: i });
3287
+ return;
3288
+ }
3289
+ const st = matchSectionTag(raw);
3290
+ if (st) {
3291
+ if (st.open) {
3292
+ const guard = parseWhenAttr(raw);
3293
+ xmlStack.push({ tag: st.open, line: i, ...guard ? { guard } : {} });
3294
+ }
3295
+ if (st.close) {
3296
+ const tag = st.close;
3297
+ let matched = false;
3298
+ for (let s = xmlStack.length - 1; s >= 0; s -= 1) {
3299
+ if (xmlStack[s]?.tag === tag) {
3300
+ const o = xmlStack.splice(s, 1)[0];
3301
+ const literal = tag.startsWith('"');
3302
+ if (o) sections.push({ tag: literal ? tag.slice(1, -1) : tag, name: `<${o.tag}>`, syntax: "xml", startLine: o.line, endLine: i + 1, depth: s, ...literal ? { literal: true } : {}, ...o.guard ? { guard: o.guard } : {} });
3303
+ matched = true;
3304
+ break;
3305
+ }
3306
+ }
3307
+ if (!matched) strayCloses.push({ tag, line: i });
3308
+ }
3309
+ return;
3310
+ }
3311
+ const h = raw.match(HEADING);
3312
+ if (h) {
3313
+ const level = (h[1] ?? "").length;
3314
+ const name = h[2] ?? "";
3315
+ while (headingStack.length && (headingStack[headingStack.length - 1]?.level ?? 0) >= level) {
3316
+ const closed = headingStack.pop();
3317
+ if (closed) closed.sec.endLine = i;
3318
+ }
3319
+ const sec = { tag: tagName(name), name: `# ${name}`, syntax: "heading", startLine: i, endLine: rawLines.length, depth: headingStack.length };
3320
+ sections.push(sec);
3321
+ headingStack.push({ level, sec });
3322
+ return;
3323
+ }
3324
+ const m = raw.trimStart().match(STEP);
3325
+ if (m) {
3326
+ const ord = m[1] ?? "";
3327
+ const segments = ord.split(".").map(Number);
3328
+ const { anchor, text } = splitAnchor(m[2] ?? "");
3329
+ steps.push({
3330
+ ord,
3331
+ segments,
3332
+ major: segments[0] ?? 0,
3333
+ parent: segments.length > 1 ? segments.slice(0, -1).join(".") : null,
3334
+ line: i,
3335
+ text,
3336
+ depth: segments.length - 1,
3337
+ anchor
3338
+ });
3339
+ }
3340
+ });
3341
+ const toolDefs = parseTools(rawLines).tools;
3342
+ const toolKeys = /* @__PURE__ */ new Set([...imports.filter((x) => x.kind === "tool").map((x) => x.key), ...toolDefs.map((t) => t.name)]);
3343
+ const routeKeys = new Set(imports.filter((x) => x.kind === "route").map((x) => x.key));
3344
+ const inputKeys = new Set(imports.filter((x) => x.kind === "variable").map((x) => x.key));
3345
+ const tests = parseTests(rawLines);
3346
+ const examples = parseExamples(rawLines);
3347
+ const aliases = [];
3348
+ {
3349
+ let inFence = false;
3350
+ for (let i = 0; i < rawLines.length; i += 1) {
3351
+ const raw = rawLines[i] ?? "";
3352
+ if (raw.trim().startsWith("```") || raw.trim().startsWith("~~~")) {
3353
+ inFence = !inFence;
3354
+ continue;
3355
+ }
3356
+ if (inFence) continue;
3357
+ const name = annotationAlias(raw);
3358
+ if (name === null) continue;
3359
+ let target = null;
3360
+ let f2 = false;
3361
+ for (let j = i + 1; j < rawLines.length; j += 1) {
3362
+ const r2 = rawLines[j] ?? "";
3363
+ const s2 = r2.trim();
3364
+ if (s2.startsWith("```") || s2.startsWith("~~~")) {
3365
+ f2 = !f2;
3366
+ continue;
3367
+ }
3368
+ if (f2) {
3369
+ target = j;
3370
+ break;
3371
+ }
3372
+ if (s2 === "" || /^\s*\/\//.test(r2) || isAnnotationLine(r2)) continue;
3373
+ target = j;
3374
+ break;
3375
+ }
3376
+ aliases.push({ name, line: i, target });
3377
+ }
3378
+ }
3379
+ return {
3380
+ steps,
3381
+ sections,
3382
+ variables,
3383
+ imports,
3384
+ fileImports,
3385
+ importLines,
3386
+ config,
3387
+ configLines,
3388
+ defines,
3389
+ defineLines,
3390
+ inputTypes,
3391
+ toolDefs,
3392
+ tests,
3393
+ testLines: testBlockLines(rawLines),
3394
+ examples,
3395
+ exampleLines: exampleBlockLines(rawLines),
3396
+ unclosedTags: xmlStack.map((o) => ({ tag: o.tag, line: o.line })),
3397
+ // opens still on the stack at EOF
3398
+ strayCloses,
3399
+ aliases,
3400
+ index: {
3401
+ steps: new Set(steps.map((s) => s.ord)),
3402
+ anchors: new Set(steps.map((s) => s.anchor).filter((a) => a !== null)),
3403
+ sections: new Set(sections.map((s) => s.tag)),
3404
+ tools: toolKeys,
3405
+ routes: routeKeys,
3406
+ inputs: inputKeys,
3407
+ aliases: new Set(aliases.map((a) => a.name))
3408
+ }
3409
+ };
3410
+ }
3411
+ function resolveAnchors(source) {
3412
+ const lines = source.split("\n");
3413
+ const anchored = buildStructure(lines).steps.filter((s) => s.anchor !== null);
3414
+ if (anchored.length === 0) return source;
3415
+ const label = /* @__PURE__ */ new Map();
3416
+ for (const s of anchored) {
3417
+ if (!s.anchor) continue;
3418
+ label.set(s.anchor, (lines[s.line] ?? "").match(/^\s*(\d+(?:\.\d+)*)/)?.[1] ?? s.ord);
3419
+ lines[s.line] = (lines[s.line] ?? "").replace(/^(\s*\d+(?:\.\d+)*[.)]\s+)#[a-z][a-z0-9_]*\s+/i, "$1");
3420
+ }
3421
+ let inFence = false;
3422
+ return lines.map((line) => {
3423
+ const t = line.trim();
3424
+ if (t.startsWith("```") || t.startsWith("~~~")) {
3425
+ inFence = !inFence;
3426
+ return line;
3427
+ }
3428
+ return inFence ? line : line.replace(/#([a-z][a-z0-9_]*)/gi, (whole, name) => label.get(name.toLowerCase()) ?? whole);
3429
+ }).join("\n");
3430
+ }
3431
+ function renumberSubSteps(lines) {
3432
+ const doc = buildStructure(lines);
3433
+ const oldToNew = /* @__PURE__ */ new Map();
3434
+ const ambiguous = /* @__PURE__ */ new Set();
3435
+ const childCount = /* @__PURE__ */ new Map();
3436
+ for (const s of [...doc.steps].sort((a, b) => a.line - b.line)) {
3437
+ let neu;
3438
+ if (s.parent === null) neu = String(s.major);
3439
+ else {
3440
+ const parentNew = oldToNew.get(s.parent) ?? s.parent;
3441
+ const n = (childCount.get(parentNew) ?? 0) + 1;
3442
+ childCount.set(parentNew, n);
3443
+ neu = `${parentNew}.${n}`;
3444
+ }
3445
+ if (oldToNew.has(s.ord) && oldToNew.get(s.ord) !== neu) ambiguous.add(s.ord);
3446
+ else oldToNew.set(s.ord, neu);
3447
+ }
3448
+ for (const a of ambiguous) oldToNew.delete(a);
3449
+ const edits = [];
3450
+ for (const s of doc.steps) {
3451
+ const neu = oldToNew.get(s.ord);
3452
+ if (neu === void 0 || neu === s.ord) continue;
3453
+ const m = (lines[s.line] ?? "").match(/^(\s*)(\d+(?:\.\d+)*)/);
3454
+ if (m) edits.push({ line: s.line, start: (m[1] ?? "").length, end: (m[1] ?? "").length + (m[2] ?? "").length, text: neu });
3455
+ }
3456
+ if (edits.length === 0) return lines;
3457
+ for (const r of findReferences(lines, doc)) {
3458
+ if (r.kind !== "step") continue;
3459
+ const neu = oldToNew.get(r.key);
3460
+ if (neu === void 0 || neu === r.key) continue;
3461
+ const text = (lines[r.line] ?? "").slice(r.start, r.end);
3462
+ const rewritten = text.replace(/\d+(?:\.\d+)*/, neu);
3463
+ if (rewritten !== text) edits.push({ line: r.line, start: r.start, end: r.end, text: rewritten });
3464
+ }
3465
+ const out = [...lines];
3466
+ const byLine = /* @__PURE__ */ new Map();
3467
+ for (const e of edits) {
3468
+ const list = byLine.get(e.line) ?? [];
3469
+ list.push(e);
3470
+ byLine.set(e.line, list);
3471
+ }
3472
+ for (const [ln, es] of byLine) {
3473
+ es.sort((a, b) => b.start - a.start);
3474
+ let text = out[ln] ?? "";
3475
+ for (const e of es) text = text.slice(0, e.start) + e.text + text.slice(e.end);
3476
+ out[ln] = text;
3477
+ }
3478
+ return out;
3479
+ }
3480
+ var STEP_REF_PATTERNS = [
3481
+ /\b(?:flow\s+)?step\s+(\d+(?:\.\d+)*)/gi,
3482
+ // step 2 / flow step 2.1.3
3483
+ /\(step\s+(\d+(?:\.\d+)*)\)/gi,
3484
+ // (step 8) — explicit keyword
3485
+ /\((\d+\.\d+(?:\.\d+)*)\)/gi,
3486
+ // (2.1) — a DOTTED ord; a bare (1)/(2) is a prose enumeration, not a step ref
3487
+ /\b(\d+(?:\.\d+)*)\s*=\s*[a-z]+/gi
3488
+ // 2.1=yes
3489
+ ];
3490
+ var STEP_ORD = /^[1-9]\d*(?:\.[1-9]\d*)*$/;
3491
+ var STEP_RANGE = /\b(\d+\.\d+(?:\.\d+)*)\s*[–-]\s*(\d+\.\d+(?:\.\d+)*)\b/g;
3492
+ var STEP_POINTER = /\b(?:to|into|goto|see|per|then|at|from)\s+(\d+\.\d+(?:\.\d+)*)\b/gi;
3493
+ var INLINE_TAG = /<([a-z][a-z0-9_]*)>/gi;
3494
+ var TRANSREF = /@<([a-z][a-z0-9_]*)((?:\.|#)[a-z][a-z0-9_]*)?>/gi;
3495
+ var TOOLINLINE = /@\[([a-z][a-z0-9_]*)\]/gi;
3496
+ var ANCHOR_REF = /#([a-z][a-z0-9_]*)/gi;
3497
+ var MARKER_PREFIX = /^\s*\d+(?:\.\d+)*[.)]\s+$/i;
3498
+ function blank(s, re) {
3499
+ return s.replace(re, (m) => " ".repeat(m.length));
3500
+ }
3501
+ function findReferences(rawLines, doc) {
3502
+ const kinds = classifyLines(rawLines);
3503
+ const xmlOpenLines = new Set(doc.sections.filter((s) => s.syntax === "xml").map((s) => s.startLine));
3504
+ const hasToolImport = doc.imports.some((x) => x.kind === "tool") || doc.toolDefs.length > 0;
3505
+ const fileImportKeys = new Set(doc.fileImports.map((f) => f.key));
3506
+ const toolMask = toolBlockMask(rawLines);
3507
+ const out = [];
3508
+ const cfgMask = configSectionMask(rawLines);
3509
+ const litMask = examplesMask(rawLines);
3510
+ let inFence = false;
3511
+ rawLines.forEach((raw, i) => {
3512
+ const t = raw.trim();
3513
+ if (t.startsWith("```") || t.startsWith("~~~")) {
3514
+ inFence = !inFence;
3515
+ return;
3516
+ }
3517
+ if (inFence || litMask[i]) return;
3518
+ if (toolMask[i]) return;
3519
+ if (cfgMask[i] && (CONFIG_SECTION_OPEN.test(raw) || CONFIG_SECTION_CLOSE.test(raw))) return;
3520
+ if (kinds[i] === "comment" || doc.importLines.has(i) || doc.configLines.has(i) || doc.defineLines.has(i) || doc.testLines.has(i) || doc.exampleLines.has(i) || isHeadingLine(raw) || isAnnotationLine(raw)) return;
3521
+ const masked = maskQuotes(raw).replace(/\s+\/\/.*$/, (m2) => " ".repeat(m2.length)).replace(/\bwhen=[^>]*(?=>)/gi, (m2) => " ".repeat(m2.length)).replace(/^\s*\$SWITCH\b.*$/i, (m2) => " ".repeat(m2.length)).replace(/^(\s*\$(?:IF|ELSE\s+IF)\b[^:]*:|\s*\$ELSE\s*:)/i, (m2) => " ".repeat(m2.length)).replace(/^(\s*(?:CASE\b[^:]*|ELSE(?:\s+IF\b[^:]*)?)\s*:)/i, (m2) => " ".repeat(m2.length));
3522
+ const covered = [];
3523
+ const overlaps = (s, e) => covered.some(([a, b]) => s < b && e > a);
3524
+ const add = (kind, s, e, key, resolved) => {
3525
+ if (overlaps(s, e)) return;
3526
+ covered.push([s, e]);
3527
+ out.push({ kind, line: i, start: s, end: e, key, resolved });
3528
+ };
3529
+ let m;
3530
+ STEP_RANGE.lastIndex = 0;
3531
+ while ((m = STEP_RANGE.exec(masked)) !== null) {
3532
+ const a = (m[1] ?? "").toLowerCase();
3533
+ const b = (m[2] ?? "").toLowerCase();
3534
+ const bStart = m.index + m[0].length - b.length;
3535
+ add("step", m.index, m.index + a.length, a, doc.index.steps.has(a));
3536
+ add("step", bStart, m.index + m[0].length, b, doc.index.steps.has(b));
3537
+ }
3538
+ for (const re of STEP_REF_PATTERNS) {
3539
+ re.lastIndex = 0;
3540
+ while ((m = re.exec(masked)) !== null) {
3541
+ const key = (m[1] ?? "").toLowerCase();
3542
+ if (!STEP_ORD.test(key)) continue;
3543
+ add("step", m.index, m.index + m[0].length, key, doc.index.steps.has(key));
3544
+ }
3545
+ }
3546
+ STEP_POINTER.lastIndex = 0;
3547
+ while ((m = STEP_POINTER.exec(masked)) !== null) {
3548
+ const key = (m[1] ?? "").toLowerCase();
3549
+ if (!STEP_ORD.test(key)) continue;
3550
+ const ordStart = m.index + m[0].length - (m[1] ?? "").length;
3551
+ add("step", ordStart, m.index + m[0].length, key, doc.index.steps.has(key));
3552
+ }
3553
+ ANCHOR_REF.lastIndex = 0;
3554
+ while ((m = ANCHOR_REF.exec(masked)) !== null) {
3555
+ if (MARKER_PREFIX.test(masked.slice(0, m.index))) continue;
3556
+ const name = (m[1] ?? "").toLowerCase();
3557
+ add("step", m.index, m.index + m[0].length, name, doc.index.anchors.has(name));
3558
+ }
3559
+ TRANSREF.lastIndex = 0;
3560
+ while ((m = TRANSREF.exec(masked)) !== null) {
3561
+ const [ts, te] = [m.index, m.index + m[0].length];
3562
+ const key = (m[1] ?? "").toLowerCase();
3563
+ const addressed = !!m[2];
3564
+ if (!addressed && doc.index.sections.has(key)) add("section", ts, te, key, true);
3565
+ else if (fileImportKeys.has(key)) add("alias", ts, te, key, true);
3566
+ else covered.push([ts, te]);
3567
+ }
3568
+ TOOLINLINE.lastIndex = 0;
3569
+ while ((m = TOOLINLINE.exec(masked)) !== null) {
3570
+ const [is, ie] = [m.index, m.index + m[0].length];
3571
+ const key = (m[1] ?? "").toLowerCase();
3572
+ if (doc.index.tools.has(key)) add("tool", is, ie, key, true);
3573
+ else covered.push([is, ie]);
3574
+ }
3575
+ if (!xmlOpenLines.has(i) && !matchSectionTag(raw)?.open) {
3576
+ INLINE_TAG.lastIndex = 0;
3577
+ while ((m = INLINE_TAG.exec(masked)) !== null) {
3578
+ const key = (m[1] ?? "").toLowerCase();
3579
+ if (masked.slice(m.index + m[0].length).toLowerCase().includes(`</${key}>`)) continue;
3580
+ add("section", m.index, m.index + m[0].length, key, doc.index.sections.has(key));
3581
+ }
3582
+ }
3583
+ VARREF.lastIndex = 0;
3584
+ while ((m = VARREF.exec(masked)) !== null) add("variable", m.index, m.index + m[0].length, (m[1] ?? "").toLowerCase(), true);
3585
+ for (const r of doc.imports) {
3586
+ if (r.kind !== "route") continue;
3587
+ let from = 0;
3588
+ const lower = masked.toLowerCase();
3589
+ let at;
3590
+ while ((at = lower.indexOf(r.key, from)) >= 0) {
3591
+ add("route", at, at + r.key.length, r.key, true);
3592
+ from = at + r.key.length;
3593
+ }
3594
+ }
3595
+ if (hasToolImport) {
3596
+ const noVars = blank(blank(masked, new RegExp(`<\\/?${TAG_NAME_SRC}(?:\\s[^>]*)?>`, "gi")), /[{[][^}\]]*[}\]]/g);
3597
+ SNAKE_RE.lastIndex = 0;
3598
+ while ((m = SNAKE_RE.exec(noVars)) !== null) {
3599
+ const key = m[0].toLowerCase();
3600
+ if (doc.index.inputs.has(key)) continue;
3601
+ add("tool", m.index, m.index + m[0].length, key, doc.index.tools.has(key));
3602
+ }
3603
+ }
3604
+ });
3605
+ return out;
3606
+ }
3607
+ var IF_BRANCH = /^\s*if\b\s*(.*)$/i;
3608
+ var OTHERWISE_BRANCH = /^\s*otherwise\b[,:]?\s*(.*)$/i;
3609
+ var DECISION_HEADER = /^(?:based|depending)\s+on\b.*:\s*$/i;
3610
+ function isDecisionHeader(text) {
3611
+ return DECISION_HEADER.test(text.trim());
3612
+ }
3613
+ function parseBranch(text) {
3614
+ const ow = text.match(OTHERWISE_BRANCH);
3615
+ if (ow) return { kind: "otherwise", guard: "", outcome: (ow[1] ?? "").trim() };
3616
+ const iff = text.match(IF_BRANCH);
3617
+ if (!iff) return null;
3618
+ const rest = iff[1] ?? "";
3619
+ const comma = rest.indexOf(",");
3620
+ return comma >= 0 ? { kind: "if", guard: rest.slice(0, comma).trim(), outcome: rest.slice(comma + 1).trim() } : { kind: "if", guard: rest.trim(), outcome: "" };
3621
+ }
3622
+ function branchGroups(doc) {
3623
+ const byOrd = /* @__PURE__ */ new Map();
3624
+ const byParent = /* @__PURE__ */ new Map();
3625
+ for (const s of doc.steps) {
3626
+ byOrd.set(s.ord, s);
3627
+ if (s.parent === null) continue;
3628
+ const list = byParent.get(s.parent) ?? [];
3629
+ list.push(s);
3630
+ byParent.set(s.parent, list);
3631
+ }
3632
+ const groups = [];
3633
+ for (const [parent, children] of byParent) {
3634
+ const branches = [];
3635
+ for (const s of [...children].sort((a, b) => a.line - b.line)) {
3636
+ const p = parseBranch(s.text);
3637
+ if (p) branches.push({ line: s.line, ord: s.ord, kind: p.kind, guard: p.guard, outcome: p.outcome });
3638
+ }
3639
+ if (branches.length === 0) continue;
3640
+ const header = byOrd.get(parent);
3641
+ groups.push({ parent, headerLine: header?.line ?? branches[0]?.line ?? 0, headerText: header?.text ?? "", branches });
3642
+ }
3643
+ return groups;
3644
+ }
3645
+ function normValue(s) {
3646
+ return s.trim().toLowerCase().replace(/[.,;:!?]+$/, "").replace(/^(?:an?|the)\s+/, "").trim();
3647
+ }
3648
+ function guardValues(guard) {
3649
+ const head = guard.trim().toLowerCase().replace(/^(?:it'?s|it\s+is)\s+/, "");
3650
+ return head.split(/\s*(?:,|\bor\b)\s*/).map(normValue).filter((s) => s !== "");
3651
+ }
3652
+ var MAX_TUPLE_PRODUCT = 256;
3653
+ function firstUncovered(domains, patterns) {
3654
+ const total = domains.reduce((n, d) => n * d.length, 1);
3655
+ for (let idx = 0; idx < total; idx += 1) {
3656
+ const tuple = [];
3657
+ let rem = idx;
3658
+ for (const d of domains) {
3659
+ tuple.push(d[rem % d.length] ?? "");
3660
+ rem = Math.floor(rem / d.length);
3661
+ }
3662
+ if (!patterns.some((p) => p.every((cell, i) => cell === "_" || cell === tuple[i]))) return tuple;
3663
+ }
3664
+ return null;
3665
+ }
3666
+ function predicateEncodable(block, inputTypes, defines) {
3667
+ const okAtom = (name, test, seen) => {
3668
+ if (defines.has(name)) {
3669
+ if (test !== "bool" || seen.has(name)) return false;
3670
+ const ast = parseExpr(defines.get(name)?.expr ?? "").ast;
3671
+ return ast !== null && exprAtoms(ast).every((a) => okAtom(a.name, a.test, /* @__PURE__ */ new Set([...seen, name])));
3672
+ }
3673
+ const t = inputTypes.get(name);
3674
+ if (!t) return false;
3675
+ return test === "bool" ? t.kind === "bool" : test === "numeric" ? t.kind === "number" : t.kind === "enum";
3676
+ };
3677
+ for (const arm of block.arms) {
3678
+ if (arm.kind !== "case" || arm.predicate === void 0) continue;
3679
+ const ast = parseExpr(arm.predicate).ast;
3680
+ if (ast === null || !exprAtoms(ast).every((a) => okAtom(a.name, a.test, /* @__PURE__ */ new Set()))) return false;
3681
+ }
3682
+ return true;
3683
+ }
3684
+ function structuralConflicts(rawLines) {
3685
+ const doc = buildStructure(rawLines);
3686
+ const refs = findReferences(rawLines, doc);
3687
+ const out = [];
3688
+ for (const t of doc.tests) {
3689
+ if (t.error !== null) out.push({ lines: [t.headerLine], message: t.error, code: "bad-test", severity: "error" });
3690
+ }
3691
+ for (const line of strayExpects(rawLines)) {
3692
+ out.push({ lines: [line], message: 'EXPECT is retired \u2014 write each expectation as a "- " sub-bullet under "- expect::" inside a $TEST block.', code: "bad-test", severity: "error" });
3693
+ }
3694
+ for (const e of doc.examples) {
3695
+ if (e.error !== null) out.push({ lines: [e.headerLine], message: e.error, code: "bad-example", severity: "error" });
3696
+ }
3697
+ const cfgMask = configSectionMask(rawLines);
3698
+ if (cfgMask.some(Boolean)) {
3699
+ const kinds2 = classifyLines(rawLines);
3700
+ const defMask = toolBlockMask(rawLines);
3701
+ rawLines.forEach((raw, i) => {
3702
+ if (!cfgMask[i]) return;
3703
+ if (CONFIG_SECTION_OPEN.test(raw) || CONFIG_SECTION_CLOSE.test(raw)) return;
3704
+ if (raw.trim() === "" || kinds2[i] === "comment") return;
3705
+ if (doc.importLines.has(i) || doc.configLines.has(i) || doc.defineLines.has(i) || doc.testLines.has(i) || defMask[i]) return;
3706
+ out.push({
3707
+ lines: [i],
3708
+ message: "Content inside <$CONFIG> never reaches the model \u2014 only $-commands ($IMPORT, $CONFIG, $TOOL, $DEFINE, $TEST) belong here. Move prompt content outside the section.",
3709
+ code: "config-section-content",
3710
+ severity: "error"
3711
+ });
3712
+ });
3713
+ }
3714
+ for (const u of doc.unclosedTags) {
3715
+ out.push({ lines: [u.line], message: `Tag <${u.tag}> is opened but never closed \u2014 add a </${u.tag}>.`, code: "unclosed-tag", severity: "error" });
3716
+ }
3717
+ for (const c of doc.strayCloses) {
3718
+ out.push({ lines: [c.line], message: `</${c.tag}> has no matching <${c.tag}> to close.`, code: "stray-close-tag", severity: "error" });
3719
+ }
3720
+ const byOrd = /* @__PURE__ */ new Map();
3721
+ for (const s of doc.steps) {
3722
+ const list = byOrd.get(s.ord);
3723
+ if (list) list.push(s);
3724
+ else byOrd.set(s.ord, [s]);
3725
+ }
3726
+ for (const [ord, list] of byOrd) {
3727
+ if (list.length > 1) out.push({ lines: list.map((s) => s.line), message: `Duplicate step "${ord}" \u2014 ${list.length} steps carry this number, so a reference to it is ambiguous. Renumber one of them.`, code: "duplicate-step", severity: "error" });
3728
+ }
3729
+ {
3730
+ const kindOf = (n) => doc.index.tools.has(n) ? "tool" : doc.index.routes.has(n) ? "route" : doc.index.inputs.has(n) ? "input" : doc.index.sections.has(n) ? "section" : doc.variables.has(n) ? "variable" : null;
3731
+ const seenAlias = /* @__PURE__ */ new Set();
3732
+ for (const a of doc.aliases) {
3733
+ if (seenAlias.has(a.name)) {
3734
+ out.push({ lines: [a.line], message: `Statement alias @@ ${a.name}: is already used by another annotation \u2014 a name can point at only one statement.`, code: "alias-collision", severity: "warn" });
3735
+ } else {
3736
+ const k = kindOf(a.name);
3737
+ if (k) out.push({ lines: [a.line], message: `Statement alias @@ ${a.name}: collides with an existing ${k} of that name. Rename the alias.`, code: "alias-collision", severity: "warn" });
3738
+ }
3739
+ seenAlias.add(a.name);
3740
+ }
3741
+ }
3742
+ {
3743
+ const named = doc.sections.filter((s) => s.control !== true);
3744
+ const REPEATABLE = /* @__PURE__ */ new Set(["example", "case", "item", "option", "step", "turn", "message", "exchange", "entry"]);
3745
+ const byTag = /* @__PURE__ */ new Map();
3746
+ for (const s of named) {
3747
+ const list = byTag.get(s.tag) ?? [];
3748
+ list.push(s);
3749
+ byTag.set(s.tag, list);
3750
+ }
3751
+ for (const [tag, list] of byTag) {
3752
+ if (list.length > 1 && !REPEATABLE.has(tag)) {
3753
+ out.push({ lines: list.map((s) => s.startLine), message: `Duplicate section "${tag}" \u2014 ${list.length} sections share this name. Give each a distinct name; the structure (and any reference to "${tag}") is ambiguous.`, code: "duplicate-section", severity: "error" });
3754
+ }
3755
+ }
3756
+ const nearDup = (a, b) => a + "s" === b || b + "s" === a || a + "es" === b || b + "es" === a || b.startsWith(`${a}_`) || a.startsWith(`${b}_`);
3757
+ const tags = [...byTag.keys()];
3758
+ for (let i = 0; i < tags.length; i += 1) {
3759
+ for (let j = i + 1; j < tags.length; j += 1) {
3760
+ const a = tags[i] ?? "";
3761
+ const b = tags[j] ?? "";
3762
+ if (!nearDup(a, b)) continue;
3763
+ const lines = named.filter((s) => s.tag === a || s.tag === b).map((s) => s.startLine);
3764
+ out.push({ lines, message: `Near-duplicate section names "${a}" and "${b}" \u2014 consolidate them or give each a clearly distinct name.`, code: "near-duplicate-section", severity: "error" });
3765
+ }
3766
+ }
3767
+ const VAGUE = /* @__PURE__ */ new Set(["section", "content", "contents", "data", "info", "text", "stuff", "misc", "part", "tmp", "temp", "block", "tag", "thing", "things", "general", "main", "body", "other", "extra"]);
3768
+ for (const s of named) {
3769
+ if (s.syntax !== "xml") continue;
3770
+ if (s.tag.length <= 2) {
3771
+ out.push({ lines: [s.startLine], message: `Section name "${s.tag}" is too short to be meaningful \u2014 name it for what it contains.`, code: "vague-section-name", severity: "warn" });
3772
+ continue;
3773
+ }
3774
+ const stem2 = s.tag.replace(/\d+$/, "");
3775
+ if (stem2 === "" || VAGUE.has(stem2)) {
3776
+ out.push({ lines: [s.startLine], message: `Section name "${s.tag}" is vague \u2014 name it for what it contains (e.g. <escalation_rules>, not <${s.tag}>).`, code: "vague-section-name", severity: "warn" });
3777
+ }
3778
+ }
3779
+ }
3780
+ const siblings = /* @__PURE__ */ new Map();
3781
+ for (const s of doc.steps) {
3782
+ const key = s.parent ?? "";
3783
+ const list = siblings.get(key) ?? [];
3784
+ list.push(s);
3785
+ siblings.set(key, list);
3786
+ }
3787
+ const lastSeg = (s) => s.segments[s.segments.length - 1] ?? 0;
3788
+ const label = (parent, n) => parent ? `${parent}.${n}` : `${n}`;
3789
+ for (const [parent, kids] of siblings) {
3790
+ const byLine = [...kids].sort((a, b) => a.line - b.line);
3791
+ const nums = [...new Set(byLine.map(lastSeg))].sort((a, b) => a - b);
3792
+ for (let k = 1; k < nums.length; k += 1) {
3793
+ const prev = nums[k - 1] ?? 0;
3794
+ const cur = nums[k] ?? 0;
3795
+ for (let g = prev + 1; g < cur; g += 1) {
3796
+ const line = byLine.find((s) => lastSeg(s) === cur)?.line ?? 0;
3797
+ out.push({ lines: [line], message: `Missing step ${label(parent, g)} \u2014 the sequence jumps from ${label(parent, prev)} to ${label(parent, cur)}.`, code: "missing-step", severity: "error" });
3798
+ }
3799
+ }
3800
+ for (let k = 1; k < byLine.length; k += 1) {
3801
+ const cur = byLine[k];
3802
+ const prevStep = byLine[k - 1];
3803
+ if (cur && prevStep && lastSeg(cur) < lastSeg(prevStep)) {
3804
+ out.push({ lines: [cur.line], message: `Step ${cur.ord} appears after step ${prevStep.ord} \u2014 steps are out of order.`, code: "unordered-step", severity: "warn" });
3805
+ }
3806
+ }
3807
+ }
3808
+ for (const r of refs) {
3809
+ if (r.resolved) continue;
3810
+ if (r.kind === "step") out.push({ lines: [r.line], message: `Reference to step ${r.key}, which is not defined.`, code: "dangling-step-ref", severity: "error" });
3811
+ else if (r.kind === "section") out.push({ lines: [r.line], message: `Reference to section <${r.key}>, which is not defined.`, code: "dangling-section-ref", severity: "error" });
3812
+ else if (r.kind === "tool") out.push({ lines: [r.line], message: `Tool "${r.key}" is used but never imported (add it to a $IMPORT tool line).`, code: "undeclared-tool", severity: "error" });
3813
+ }
3814
+ const guardVars = /* @__PURE__ */ new Set();
3815
+ for (const sec of doc.sections) {
3816
+ if (sec.syntax !== "xml" || !sec.guard) continue;
3817
+ const parsed = parseExpr(sec.guard.text).ast;
3818
+ if (parsed) for (const a of exprAtoms(parsed)) guardVars.add(a.name);
3819
+ }
3820
+ for (const d of doc.defines.values()) {
3821
+ const parsed = parseExpr(d.expr).ast;
3822
+ if (parsed) for (const a of exprAtoms(parsed)) guardVars.add(a.name);
3823
+ }
3824
+ const switches = parseSwitches(rawLines);
3825
+ for (const b of switches) {
3826
+ guardVars.add(b.varName);
3827
+ for (const n of b.varNames ?? []) guardVars.add(n);
3828
+ for (const a of b.arms) {
3829
+ if (a.predicate === void 0) continue;
3830
+ const parsed = parseExpr(a.predicate).ast;
3831
+ if (parsed) for (const at of exprAtoms(parsed)) guardVars.add(at.name);
3832
+ }
3833
+ }
3834
+ const inlineSwitches = parseInlineSwitches(rawLines);
3835
+ for (const sw of inlineSwitches) guardVars.add(sw.varName);
3836
+ const referenced = /* @__PURE__ */ new Set([...refs.map((r) => r.key), ...guardVars]);
3837
+ const isCondVar = (name) => doc.index.inputs.has(name) || doc.defines.has(name);
3838
+ const condType = (name) => doc.inputTypes.get(name) ?? (doc.defines.has(name) ? { kind: "bool" } : void 0);
3839
+ const modular = rawLines.some((l) => IMPORT_FILE_RE.test(l));
3840
+ for (const im of doc.imports) {
3841
+ if (!modular && !referenced.has(im.key)) out.push({ lines: [im.line], message: im.kind === "variable" ? `Required variable "${im.name}" is never used.` : `Imported ${im.kind} "${im.name}" is never used.`, code: "unused-import", severity: "warn" });
3842
+ }
3843
+ out.push(...parseTools(rawLines).conflicts);
3844
+ const importedTools = new Map(doc.imports.filter((x) => x.kind === "tool").map((x) => [x.key, x.line]));
3845
+ for (const t of doc.toolDefs) {
3846
+ const importLine = importedTools.get(t.name);
3847
+ if (importLine !== void 0) {
3848
+ out.push({ lines: [t.line, importLine], message: `Tool "${t.name}" is both DEFINED here and $IMPORTed \u2014 pick one: drop the import (it's defined in-file) or the definition.`, code: "tool-defined-and-imported", severity: "warn" });
3849
+ }
3850
+ if (!referenced.has(t.name)) {
3851
+ out.push({ lines: [t.line], message: `Defined tool "${t.name}" is never used in the prompt \u2014 the model cannot discover a tool no rule mentions. Reference it in a rule (e.g. "call ${t.name} when ...") or delete the definition.`, code: "unused-tool", severity: "warn" });
3852
+ }
3853
+ }
3854
+ for (const line of doc.importLines) {
3855
+ const raw = rawLines[line] ?? "";
3856
+ const rq = raw.match(REQUIRE_RE);
3857
+ const im = rq ? null : raw.match(IMPORT_RE);
3858
+ const payload = rq ? rq[1] ?? null : im && im[1] === "input" ? im[2] ?? null : null;
3859
+ if (payload === null || !payload.includes(":")) continue;
3860
+ const { error } = parseTypedInput(payload);
3861
+ if (error !== null) out.push({ lines: [line], message: error, code: "bad-input-type", severity: "error" });
3862
+ }
3863
+ for (const sec of doc.sections) {
3864
+ if (sec.syntax !== "xml" || !sec.guard) continue;
3865
+ const at = sec.startLine;
3866
+ const { ast, error } = parseExpr(sec.guard.text);
3867
+ if (error !== null || ast === null) {
3868
+ out.push({ lines: [at], message: `Guard on <${sec.tag}> is not a valid condition: ${error ?? "parse error"}.`, code: "bad-guard-expr", severity: "error" });
3869
+ continue;
3870
+ }
3871
+ for (const atom of exprAtoms(ast)) {
3872
+ if (!isCondVar(atom.name)) {
3873
+ out.push({ lines: [at], message: `Guard on <${sec.tag}> reads @{${atom.name}}, which isn't a declared input or $DEFINE.`, code: "undeclared-input", severity: "error" });
3874
+ continue;
3875
+ }
3876
+ const type = condType(atom.name);
3877
+ if (type === void 0) continue;
3878
+ if (atom.test === "bool" && type.kind !== "bool") {
3879
+ out.push({ lines: [at], message: `Guard on <${sec.tag}> uses @{${atom.name}} as a boolean, but it's ${type.kind === "enum" ? "an enum" : `a ${type.kind}`} \u2014 compare it, e.g. ${type.kind === "number" ? `\`@{${atom.name}} is at least <n>\`` : `\`@{${atom.name}} is equal to <value>\``}.`, code: "guard-type-mismatch", severity: "error" });
3880
+ } else if (atom.test === "value" && type.kind === "bool") {
3881
+ out.push({ lines: [at], message: `Guard on <${sec.tag}> compares @{${atom.name}} to a value, but it's a bool \u2014 use \`@{${atom.name}}\` or \`not @{${atom.name}}\`.`, code: "guard-type-mismatch", severity: "error" });
3882
+ } else if (atom.test === "value" && type.kind === "enum") {
3883
+ const bad = atom.values.filter((v) => !type.members.includes(v));
3884
+ if (bad.length > 0) out.push({ lines: [at], message: `Guard on <${sec.tag}>: @{${atom.name}} is never ${bad.join(", ")} (it's one of ${type.members.join(", ")}).`, code: "impossible-value", severity: "error" });
3885
+ } else if (atom.test === "numeric" && type.kind !== "number") {
3886
+ out.push({ lines: [at], message: `Guard on <${sec.tag}> compares @{${atom.name}} numerically (is greater than / is at least \u2026), but it's ${type.kind === "enum" ? "an enum" : `a ${type.kind}`} \u2014 a numeric comparison needs a number input.`, code: "guard-type-mismatch", severity: "error" });
3887
+ }
3888
+ }
3889
+ }
3890
+ for (const block of switches) {
3891
+ const at = block.headerLine;
3892
+ const isBlock = block.form === "block";
3893
+ const fallbackName = isBlock ? '"- otherwise::"' : "ELSE";
3894
+ if (block.error !== null) {
3895
+ out.push({ lines: [at], message: block.error, code: block.errorCode ?? "bad-switch", severity: "error" });
3896
+ continue;
3897
+ }
3898
+ const elseIdx = block.arms.findIndex((a) => a.kind === "else");
3899
+ for (const a of elseIdx >= 0 ? block.arms.slice(elseIdx + 1) : []) {
3900
+ out.push({ lines: [a.headerLine], message: `This arm comes after ${fallbackName}, so it can never run \u2014 move it above the ${isBlock ? "otherwise row" : "ELSE"}.`, code: "unreachable-case", severity: "error" });
3901
+ }
3902
+ const hasElse = elseIdx >= 0;
3903
+ if (block.kind === "predicate") {
3904
+ for (const a of block.arms) {
3905
+ if (a.kind !== "case" || a.predicate === void 0) continue;
3906
+ const { ast, error } = parseExpr(a.predicate);
3907
+ if (error !== null || ast === null) {
3908
+ out.push({ lines: [a.headerLine], message: `This arm's condition doesn't parse: ${error ?? "parse error"}.`, code: "bad-case-expr", severity: "error" });
3909
+ continue;
3910
+ }
3911
+ for (const atom of exprAtoms(ast)) {
3912
+ if (!isCondVar(atom.name)) {
3913
+ out.push({ lines: [a.headerLine], message: `This arm's condition reads @{${atom.name}}, which isn't a declared input or $DEFINE.`, code: "undeclared-input", severity: "error" });
3914
+ continue;
3915
+ }
3916
+ const t = condType(atom.name);
3917
+ if (t === void 0) continue;
3918
+ if (atom.test === "bool" && t.kind !== "bool") out.push({ lines: [a.headerLine], message: `This arm's condition uses @{${atom.name}} as a boolean, but it's ${t.kind === "enum" ? "an enum" : `a ${t.kind}`} \u2014 compare it, e.g. ${t.kind === "number" ? `\`@{${atom.name}} is at least <n>\`` : `\`@{${atom.name}} is equal to <value>\``}.`, code: "guard-type-mismatch", severity: "error" });
3919
+ else if (atom.test === "value" && t.kind === "bool") out.push({ lines: [a.headerLine], message: `This arm's condition compares @{${atom.name}} to a value, but it's a bool \u2014 use \`@{${atom.name}}\` or \`not @{${atom.name}}\`.`, code: "guard-type-mismatch", severity: "error" });
3920
+ else if (atom.test === "value" && t.kind === "enum") {
3921
+ const bad = atom.values.filter((v) => !t.members.includes(v));
3922
+ if (bad.length > 0) out.push({ lines: [a.headerLine], message: `This arm's condition: @{${atom.name}} is never ${bad.join(", ")} (it's one of ${t.members.join(", ")}).`, code: "impossible-value", severity: "error" });
3923
+ } else if (atom.test === "numeric" && t.kind !== "number") out.push({ lines: [a.headerLine], message: `This arm's condition compares @{${atom.name}} numerically (is greater than / is at least \u2026), but it's ${t.kind === "enum" ? "an enum" : `a ${t.kind}`} \u2014 a numeric comparison needs a number input.`, code: "guard-type-mismatch", severity: "error" });
3924
+ }
3925
+ }
3926
+ if (!hasElse && !predicateEncodable(block, doc.inputTypes, doc.defines))
3927
+ out.push({ lines: [at], message: `A chain over open-ended conditions can't be proven exhaustive \u2014 add an ELSE arm.`, code: "non-exhaustive-switch", severity: "error" });
3928
+ continue;
3929
+ }
3930
+ if (block.kind === "tuple") {
3931
+ const vars = block.varNames ?? [];
3932
+ const undeclared = vars.filter((n) => !isCondVar(n));
3933
+ if (undeclared.length > 0) {
3934
+ for (const n of undeclared) out.push({ lines: [at], message: `This chain reads @{${n}}, which isn't a declared input or $DEFINE.`, code: "undeclared-input", severity: "error" });
3935
+ continue;
3936
+ }
3937
+ const domains = vars.map((n) => {
3938
+ const t = condType(n);
3939
+ return t?.kind === "enum" ? t.members : t?.kind === "bool" ? ["true", "false"] : null;
3940
+ });
3941
+ const seen = /* @__PURE__ */ new Set();
3942
+ const covering = [];
3943
+ for (const a of block.arms) {
3944
+ if (a.kind !== "case" || a.patterns === void 0) continue;
3945
+ a.patterns.forEach((p, i) => {
3946
+ const dom = domains[i];
3947
+ if (p !== "_" && dom && !dom.includes(p)) out.push({ lines: [a.headerLine], message: `@{${vars[i]}} is never ${p} (it's one of ${dom.join(", ")}).`, code: "impossible-case", severity: "error" });
3948
+ });
3949
+ const key = JSON.stringify(a.patterns);
3950
+ if (seen.has(key)) out.push({ lines: [a.headerLine], message: `The combination (${a.patterns.join(", ")}) is matched by an earlier arm \u2014 this arm can never run.`, code: "duplicate-case", severity: "error" });
3951
+ else {
3952
+ seen.add(key);
3953
+ covering.push(a.patterns);
3954
+ }
3955
+ }
3956
+ if (!hasElse) {
3957
+ const open = domains.some((d) => d === null);
3958
+ const product = open ? Infinity : domains.reduce((n, d) => n * d.length, 1);
3959
+ if (open || product > MAX_TUPLE_PRODUCT) {
3960
+ out.push({ lines: [at], message: `This chain on ${vars.map((v) => `@{${v}}`).join(", ")} ranges over ${open ? "an open domain" : "too many combinations"} to enumerate \u2014 add an ELSE arm.`, code: "opaque-switch", severity: "error" });
3961
+ } else {
3962
+ const gap = firstUncovered(domains, covering);
3963
+ if (gap) out.push({ lines: [at], message: `No arm handles ${vars.map((v, i) => `@{${v}}=${gap[i]}`).join(", ")} \u2014 add an arm or an ELSE.`, code: "non-exhaustive-switch", severity: "error" });
3964
+ }
3965
+ }
3966
+ continue;
3967
+ }
3968
+ if (!isCondVar(block.varName)) {
3969
+ out.push({ lines: [at], message: `This ${isBlock ? "$SWITCH reads" : "chain reads"} @{${block.varName}}, which isn't a declared input or $DEFINE.`, code: "undeclared-input", severity: "error" });
3970
+ continue;
3971
+ }
3972
+ const firstUse = /* @__PURE__ */ new Map();
3973
+ for (const a of block.arms) {
3974
+ if (a.kind !== "case") continue;
3975
+ for (const v of a.values) {
3976
+ const prev = firstUse.get(v);
3977
+ if (prev === void 0) firstUse.set(v, a.headerLine);
3978
+ else out.push({ lines: [a.headerLine, prev], message: `${isBlock ? `Member "${v}" already has an arm` : `Value "${v}" is matched by more than one arm`} \u2014 only the first can run.`, code: "duplicate-case", severity: "error" });
3979
+ }
3980
+ }
3981
+ const covered = new Set(firstUse.keys());
3982
+ const type = condType(block.varName);
3983
+ const domain = type?.kind === "enum" ? type.members : type?.kind === "bool" ? ["true", "false"] : null;
3984
+ if (domain !== null) {
3985
+ for (const a of block.arms) {
3986
+ if (a.kind !== "case") continue;
3987
+ const bad = a.values.filter((v) => !domain.includes(v));
3988
+ if (bad.length > 0)
3989
+ out.push({
3990
+ lines: [a.headerLine],
3991
+ message: isBlock ? `"${bad.join('", "')}" is not a member of @{${block.varName}}'s domain \u2014 one of ${domain.join(", ")}.` : `@{${block.varName}} is never ${bad.join(", ")} (it's one of ${domain.join(", ")}).`,
3992
+ code: "impossible-case",
3993
+ severity: "error"
3994
+ });
3995
+ }
3996
+ if (!hasElse) {
3997
+ const missing = domain.filter((m) => !covered.has(m));
3998
+ if (missing.length > 0)
3999
+ out.push({
4000
+ lines: [at],
4001
+ message: isBlock ? `@{${block.varName}} can be ${missing.join(", ")}, but no arm handles ${missing.length > 1 ? "them" : "it"} \u2014 add a "- <member>::" row for each (or a deliberate "- otherwise::" fallback).` : `@{${block.varName}} can be ${missing.join(", ")}, but no arm handles ${missing.length > 1 ? "them" : "it"} \u2014 add an arm or an ELSE.`,
4002
+ code: "non-exhaustive-switch",
4003
+ severity: "error"
4004
+ });
4005
+ } else if (isBlock) {
4006
+ const missing = domain.filter((m) => !covered.has(m));
4007
+ const elseArm = block.arms[elseIdx];
4008
+ if (missing.length === 0 && elseArm !== void 0) {
4009
+ out.push({ lines: [elseArm.headerLine], message: `Every member of @{${block.varName}} has its own arm \u2014 "- otherwise::" can never run, and it would silently absorb members added to the domain later. Remove the row.`, code: "unreachable-otherwise", severity: "error" });
4010
+ }
4011
+ }
4012
+ } else if (!hasElse) {
4013
+ out.push({ lines: [at], message: isBlock ? `@{${block.varName}} has an open domain (${type?.kind ?? "untyped"}) \u2014 a $SWITCH on it needs a "- otherwise::" row (members can't cover an open domain).` : `@{${block.varName}} has an open domain (${type?.kind ?? "untyped"}) \u2014 a chain on it needs an ELSE arm.`, code: "opaque-switch", severity: "error" });
4014
+ }
4015
+ if (!isBlock && domain !== null && block.arms.length >= 2 && block.arms.every((a) => a.kind === "else" || a.values.length === 1 && a.inlineBody !== "" && a.headerLine + 1 === a.spanEnd)) {
4016
+ const pad = " ".repeat(block.indent);
4017
+ const missing = domain.filter((m) => !covered.has(m));
4018
+ const rows = block.arms.filter((a) => a.kind === "case" || missing.length > 0).map((a) => a.kind === "else" ? `${pad} - otherwise:: ${a.inlineBody}` : `${pad} - ${a.values[0]}:: ${a.inlineBody}`);
4019
+ out.push({
4020
+ lines: [at],
4021
+ message: `Every arm tests @{${block.varName}} for one member \u2014 this chain is a $SWITCH in disguise. The block form proves coverage by construction (a member added to the domain later fails loudly here instead of falling through). Replace the chain with:
4022
+ ${pad}$SWITCH ON @{${block.varName}}
4023
+ ${rows.join("\n")}`,
4024
+ code: "switch-shaped-chain",
4025
+ severity: "info"
4026
+ });
4027
+ }
4028
+ if (isBlock) {
4029
+ const armStmts = block.arms.map((a) => {
4030
+ const stmts = [];
4031
+ if (a.inlineBody !== "") stmts.push({ text: a.inlineBody.trim(), line: a.headerLine });
4032
+ for (let l = a.headerLine + 1; l < a.spanEnd; l += 1) {
4033
+ const t = (rawLines[l] ?? "").trim();
4034
+ if (t === "" || t.startsWith("//") || t.startsWith("@@")) continue;
4035
+ stmts.push({ text: t, line: l });
4036
+ }
4037
+ return stmts;
4038
+ });
4039
+ if (block.arms.length >= 2) {
4040
+ const first = armStmts[0] ?? [];
4041
+ for (const s of first) {
4042
+ if (armStmts.every((stmts) => stmts.some((x) => x.text === s.text))) {
4043
+ const sites = armStmts.map((stmts) => stmts.find((x) => x.text === s.text)?.line).filter((l) => l !== void 0);
4044
+ out.push({ lines: sites, message: `Every arm repeats "${s.text}" \u2014 shared content is branch-independent, so hoist it above the $SWITCH.`, code: "hoist-arm", severity: "info" });
4045
+ }
4046
+ }
4047
+ }
4048
+ block.arms.forEach((a, i) => {
4049
+ const n = (armStmts[i] ?? []).length;
4050
+ if (a.kind === "case" && n > 8) {
4051
+ out.push({ lines: [a.headerLine], message: `This arm carries ${n} statements \u2014 extract the body to a section and point: "- ${a.values[0] ?? "member"}:: @<section_name>".`, code: "large-arm", severity: "info" });
4052
+ }
4053
+ });
4054
+ }
4055
+ }
4056
+ for (const sw of inlineSwitches) {
4057
+ const at = sw.line;
4058
+ const raw = rawLines[at] ?? "";
4059
+ const pad = " ".repeat(raw.length - raw.trimStart().length);
4060
+ const sentence = (frag) => (raw.slice(0, sw.start) + frag + raw.slice(sw.end)).trim();
4061
+ const arms = [];
4062
+ for (const c of sw.cases) {
4063
+ if (c.isDefault) arms.push(`${pad}$ELSE: ${sentence(c.frag)}`);
4064
+ else {
4065
+ const cond = c.values.length > 1 ? `@{${sw.varName}} is one of ${c.values.join(", ")}` : `@{${sw.varName}} is equal to ${c.values[0] ?? ""}`;
4066
+ arms.push(`${pad}${arms.length === 0 ? "$IF" : "$ELSE IF"} ${cond}: ${sentence(c.frag)}`);
4067
+ }
4068
+ }
4069
+ const rewrite = arms.length > 0 ? ` Replace the line with:
4070
+ ${arms.join("\n")}` : "";
4071
+ out.push({ lines: [at], message: `The inline $SWITCH is retired \u2014 a deterministic branch is a block $IF chain, one full sentence per arm.${rewrite}`, code: "bad-switch", severity: "error" });
4072
+ }
4073
+ const seenDefine = /* @__PURE__ */ new Map();
4074
+ for (const line of doc.defineLines) {
4075
+ const raw = rawLines[line] ?? "";
4076
+ const name = (raw.match(DEFINE_RE)?.[1] ?? "").toLowerCase();
4077
+ if (name === "") continue;
4078
+ const legacy = raw.match(/^(\s*)\$DEFINE\s+([a-z][a-z0-9_]*)\s*=\s*(\S.*)$/i);
4079
+ if (legacy) {
4080
+ out.push({ lines: [line], message: `The $DEFINE binder is AS, never "=". Replace this line with: "${legacy[1] ?? ""}$DEFINE ${legacy[2] ?? ""} AS ${(legacy[3] ?? "").trim()}".`, code: "define-binder", severity: "error" });
4081
+ }
4082
+ const prev = seenDefine.get(name);
4083
+ if (prev !== void 0) out.push({ lines: [line, prev], message: `$DEFINE ${name} is defined more than once.`, code: "duplicate-define", severity: "error" });
4084
+ else seenDefine.set(name, line);
4085
+ }
4086
+ const defineDeps = /* @__PURE__ */ new Map();
4087
+ for (const [name, d] of doc.defines) {
4088
+ const { ast, error } = parseExpr(d.expr);
4089
+ if (error !== null || ast === null) {
4090
+ out.push({ lines: [d.line], message: `$DEFINE ${name} is not a valid condition: ${error ?? "parse error"}.`, code: "bad-define-expr", severity: "error" });
4091
+ defineDeps.set(name, []);
4092
+ continue;
4093
+ }
4094
+ const atoms = exprAtoms(ast);
4095
+ defineDeps.set(name, atoms.map((a) => a.name).filter((n) => doc.defines.has(n)));
4096
+ for (const atom of atoms) {
4097
+ if (!isCondVar(atom.name)) {
4098
+ out.push({ lines: [d.line], message: `$DEFINE ${name} reads @{${atom.name}}, which isn't a declared input or $DEFINE.`, code: "undeclared-input", severity: "error" });
4099
+ continue;
4100
+ }
4101
+ const type = condType(atom.name);
4102
+ if (type === void 0) continue;
4103
+ if (atom.test === "bool" && type.kind !== "bool") out.push({ lines: [d.line], message: `$DEFINE ${name} uses @{${atom.name}} as a boolean, but it's ${type.kind === "enum" ? "an enum" : `a ${type.kind}`} \u2014 compare it, e.g. ${type.kind === "number" ? `\`@{${atom.name}} is at least <n>\`` : `\`@{${atom.name}} is equal to <value>\``}.`, code: "guard-type-mismatch", severity: "error" });
4104
+ else if (atom.test === "value" && type.kind === "bool") out.push({ lines: [d.line], message: `$DEFINE ${name} compares @{${atom.name}} to a value, but it's a bool.`, code: "guard-type-mismatch", severity: "error" });
4105
+ else if (atom.test === "value" && type.kind === "enum") {
4106
+ const bad = atom.values.filter((v) => !type.members.includes(v));
4107
+ if (bad.length > 0) out.push({ lines: [d.line], message: `$DEFINE ${name}: @{${atom.name}} is never ${bad.join(", ")} (it's one of ${type.members.join(", ")}).`, code: "impossible-value", severity: "error" });
4108
+ } else if (atom.test === "numeric" && type.kind !== "number") out.push({ lines: [d.line], message: `$DEFINE ${name} compares @{${atom.name}} numerically (is greater than / is at least \u2026), but it's ${type.kind === "enum" ? "an enum" : `a ${type.kind}`} \u2014 a numeric comparison needs a number input.`, code: "guard-type-mismatch", severity: "error" });
4109
+ }
4110
+ if (!referenced.has(name)) out.push({ lines: [d.line], message: `$DEFINE ${name} is never used.`, code: "unused-define", severity: "warn" });
4111
+ }
4112
+ for (const [start, d] of doc.defines) {
4113
+ const seen = /* @__PURE__ */ new Set();
4114
+ const stack = [...defineDeps.get(start) ?? []];
4115
+ let cyclic = false;
4116
+ while (stack.length > 0) {
4117
+ const n = stack.pop() ?? "";
4118
+ if (n === start) {
4119
+ cyclic = true;
4120
+ break;
4121
+ }
4122
+ if (seen.has(n)) continue;
4123
+ seen.add(n);
4124
+ stack.push(...defineDeps.get(n) ?? []);
4125
+ }
4126
+ if (cyclic) out.push({ lines: [d.line], message: `$DEFINE ${start} depends on itself \u2014 remove the cycle.`, code: "define-cycle", severity: "error" });
4127
+ }
4128
+ const enums = rawLines.map((l) => findEnumDeclaration(l)).filter((e) => e !== null).map((e) => ({ name: e.name, members: new Set(e.members.map(normValue)) }));
4129
+ for (const g of branchGroups(doc)) {
4130
+ const owIdx = g.branches.findIndex((b) => b.kind === "otherwise");
4131
+ const ifs = g.branches.filter((b) => b.kind === "if");
4132
+ const perBranch = ifs.map((b) => new Set(guardValues(b.guard)));
4133
+ const allVals = perBranch.flatMap((s) => [...s]);
4134
+ const on = allVals.length > 0 ? enums.filter((e) => allVals.every((v) => e.members.has(v))) : [];
4135
+ const enumOf = on.length === 1 ? on[0] : void 0;
4136
+ const isDecision = enumOf !== void 0 || isDecisionHeader(g.headerText);
4137
+ for (const b of owIdx >= 0 ? g.branches.slice(owIdx + 1) : []) {
4138
+ out.push({ lines: [b.line], message: `This branch comes after "Otherwise", so it can never run \u2014 move it above the Otherwise.`, code: "unreachable-branch", severity: "error" });
4139
+ }
4140
+ if (isDecision) {
4141
+ const firstUse = /* @__PURE__ */ new Map();
4142
+ const warned = /* @__PURE__ */ new Set();
4143
+ perBranch.forEach((vals, i) => {
4144
+ for (const v of vals) {
4145
+ const prev = firstUse.get(v);
4146
+ if (prev === void 0) firstUse.set(v, i);
4147
+ else if (!warned.has(v)) {
4148
+ warned.add(v);
4149
+ out.push({ lines: [ifs[i]?.line ?? g.headerLine, ifs[prev]?.line ?? g.headerLine], message: `"${v}" is handled by more than one branch \u2014 in a decision only one runs, and an LLM won't reliably pick the first. Make the branches mutually exclusive.`, code: "overlapping-guard", severity: "warn" });
4150
+ }
4151
+ }
4152
+ });
4153
+ }
4154
+ if (owIdx >= 0) continue;
4155
+ if (enumOf) {
4156
+ const covered = new Set(allVals);
4157
+ const missing = [...enumOf.members].filter((v) => !covered.has(v));
4158
+ if (missing.length > 0) {
4159
+ out.push({
4160
+ lines: [g.headerLine],
4161
+ message: `${enumOf.name} can be ${missing.join(", ")}, but no branch handles ${missing.length > 1 ? "them" : "it"} \u2014 add a branch or an "Otherwise".`,
4162
+ code: "non-exhaustive-branch",
4163
+ severity: "error"
4164
+ });
4165
+ }
4166
+ } else if (isDecisionHeader(g.headerText)) {
4167
+ out.push({
4168
+ lines: [g.headerLine],
4169
+ message: `This decision has no "Otherwise" \u2014 a state matching none of the branches would be unhandled. Add an "Otherwise" branch.`,
4170
+ code: "no-default-branch",
4171
+ severity: "error"
4172
+ });
4173
+ }
4174
+ }
4175
+ const kinds = classifyLines(rawLines);
4176
+ const cmdToolMask = toolBlockMask(rawLines);
4177
+ let firstContent = rawLines.length;
4178
+ for (let i = 0; i < rawLines.length; i += 1) {
4179
+ if (kinds[i] !== "content" || cmdToolMask[i] || cfgMask[i]) continue;
4180
+ const line = rawLines[i] ?? "";
4181
+ if (IMPORT_RE.test(line) || IMPORT_FILE_RE.test(line) || REQUIRE_RE.test(line) || CONFIG_RE.test(line)) continue;
4182
+ firstContent = i;
4183
+ break;
4184
+ }
4185
+ for (const line of [...doc.importLines, ...doc.configLines]) {
4186
+ if (line >= firstContent && !cfgMask[line]) {
4187
+ out.push({ lines: [line], message: `A $-command must sit at the top of the file, outside any content section (it currently follows content on line ${firstContent + 1}).`, code: "misplaced-command", severity: "error" });
4188
+ }
4189
+ }
4190
+ rawLines.forEach((raw, i) => {
4191
+ if (kinds[i] !== "content" || cmdToolMask[i] || !BARE_COMMAND_RE.test(raw)) return;
4192
+ const kw = /^\s*IMPORT\b/.test(raw) ? "$IMPORT" : /^\s*TOOL\b/.test(raw) ? "$TOOL" : /^\s*SERVICE\b/.test(raw) ? "$SERVICE" : /^\s*DEFINE\b/.test(raw) ? "$DEFINE" : "$CONFIG";
4193
+ out.push({ lines: [i], message: `Commands must start with "$" \u2014 did you mean "${kw}"?`, code: "needs-sigil", severity: "error" });
4194
+ });
4195
+ return out;
4196
+ }
4197
+ function structure(source) {
4198
+ const rawLines = source.split("\n");
4199
+ const doc = buildStructure(rawLines);
4200
+ const refs = findReferences(rawLines, doc);
4201
+ const conflicts = structuralConflicts(rawLines);
4202
+ const declarations = [];
4203
+ const byId = {};
4204
+ const reg = (d) => {
4205
+ declarations.push(d);
4206
+ byId[d.id] = d;
4207
+ };
4208
+ for (const s of doc.steps) {
4209
+ const decl = { id: `step:${s.ord}`, kind: "step", label: s.ord, loc: { line: s.line, start: 0, end: 0 }, depth: s.depth, ordinal: s.major, ...s.parent ? { parentId: `step:${s.parent}` } : {} };
4210
+ reg(decl);
4211
+ if (s.anchor) byId[`step:${s.anchor}`] = decl;
4212
+ }
4213
+ for (const s of doc.sections) reg({ id: `section:${s.tag}`, kind: "section", label: s.tag, loc: { line: s.startLine, start: 0, end: 0 }, depth: s.depth });
4214
+ for (const im of doc.imports) reg({ id: `${im.kind}:${im.key}`, kind: im.kind, label: im.name, loc: { line: im.line, start: 0, end: 0 }, depth: 0 });
4215
+ for (const f of doc.fileImports) {
4216
+ const id = `alias:${f.key}`;
4217
+ if (!byId[id]) reg({ id, kind: "alias", label: f.alias, loc: { line: f.line, start: 0, end: 0 }, depth: 0 });
4218
+ }
4219
+ for (const t of doc.toolDefs) {
4220
+ const id = `tool:${t.name.toLowerCase()}`;
4221
+ if (!byId[id]) reg({ id, kind: "tool", label: t.name, loc: { line: t.line, start: 0, end: 0 }, depth: 0 });
4222
+ }
4223
+ const declaredVarKeys = new Set(doc.imports.filter((im) => im.kind === "variable").map((im) => im.key));
4224
+ for (const v of doc.variables.values()) {
4225
+ if (declaredVarKeys.has(v.base)) continue;
4226
+ const first = v.notations[0];
4227
+ if (first) reg({ id: `variable:${v.base}`, kind: "variable", label: v.base, loc: { line: first.line, start: first.start, end: first.end }, depth: 0 });
4228
+ }
4229
+ for (const a of doc.aliases) if (!byId[`alias:${a.name}`]) reg({ id: `alias:${a.name}`, kind: "alias", label: a.name, loc: { line: a.line, start: 0, end: 0 }, depth: 0 });
4230
+ const references = refs.map((r) => {
4231
+ const id = `${r.kind}:${r.key}`;
4232
+ return { loc: { line: r.line, start: r.start, end: r.end }, targetId: r.resolved && byId[id] ? id : null };
4233
+ });
4234
+ const perLine = rawLines.map(() => ({ depth: 0, sectionId: null }));
4235
+ for (const s of doc.sections) {
4236
+ for (let l = s.startLine; l < s.endLine && l < perLine.length; l += 1) {
4237
+ const p = perLine[l];
4238
+ if (!p) continue;
4239
+ p.depth = Math.max(p.depth, s.depth);
4240
+ p.sectionId = `section:${s.tag}`;
4241
+ }
4242
+ }
4243
+ const sortedSteps = [...doc.steps].sort((a, b) => a.line - b.line);
4244
+ sortedSteps.forEach((s, k) => {
4245
+ if (s.depth === 0) return;
4246
+ const sec = doc.sections.filter((x) => s.line >= x.startLine && s.line < x.endLine).reduce((best, x) => !best || x.depth > best.depth ? x : best, void 0);
4247
+ let end = sortedSteps[k + 1]?.line ?? rawLines.length;
4248
+ if (sec) end = Math.min(end, sec.endLine);
4249
+ for (let l = s.line; l < end && l < perLine.length; l += 1) {
4250
+ const p = perLine[l];
4251
+ if (p) p.depth = Math.max(p.depth, (sec?.depth ?? 0) + s.depth);
4252
+ }
4253
+ });
4254
+ return { declarations, references, byId, perLine, wellFormed: !conflicts.some((c) => c.severity === "error") };
4255
+ }
4256
+ var COMPLETION_TRIGGERS = ["$", "<", "{", "[", "@", "#", " ", ":"];
4257
+
4258
+ export { BUILTIN_CONFIG_SCHEMA, COMPLETION_TRIGGERS, MANY_CLASS, NON_ACTION_LEADS, PRIM_TYPES, REFINEMENT_KEYWORDS, addresseeSubjects, aliasMap, boundText, buildStructure, checkModel, classifyLines, collectObjects, compileToXml, configParamDiagnostics, configRuleDiagnostics, evalExpr, exampleBlockLines, exprAtoms, extractSuite, findEnumDeclaration, headingsOutsideSections, isHeadingLine, looseContentLines, maskInlineSwitches, matchSectionTag, parseActionCount, parseCardinalBody, parseConfigBlocks, parseConfigRules, parseExpr, parseInlineSwitches, parseServices, parseSwitches, parseTools, pluralizeNoun, predicateEncodable, renumberSubSteps, resolveAnchors, resolveConfigSchema, resolveDeclarations, resolveInlineText, rubricExpectLines, selectArm, selectPredicateArm, selectTupleArm, singularizeNoun, structuralConflicts, structure, switchBlockMask, testBlockLines, toolInputSchema };
4259
+ //# sourceMappingURL=chunk-YH4ZLQ4G.js.map
4260
+ //# sourceMappingURL=chunk-YH4ZLQ4G.js.map