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,3573 @@
1
+ import { classifyLines, switchBlockMask, parseSwitches, testBlockLines, exampleBlockLines, buildStructure, matchSectionTag, addresseeSubjects, parseTools, resolveDeclarations, aliasMap, structuralConflicts, resolveConfigSchema, BUILTIN_CONFIG_SCHEMA, parseConfigRules, configParamDiagnostics, configRuleDiagnostics, looseContentLines, headingsOutsideSections, checkModel, maskInlineSwitches, parseExpr, evalExpr, selectPredicateArm, selectTupleArm, selectArm, exprAtoms, resolveInlineText, parseServices, parseConfigBlocks, compileToXml, resolveAnchors, extractSuite, rubricExpectLines, findEnumDeclaration, collectObjects, singularizeNoun, pluralizeNoun, boundText, isHeadingLine, parseInlineSwitches, parseActionCount, parseCardinalBody } from './chunk-YH4ZLQ4G.js';
2
+ import { MODAL_KEYWORDS, COMPARISON_SYMBOLS, FREQUENCY_WORDS, QUANTIFIER_WORDS, keywordAlternation, GUARD_OPENERS, illustrationZoneMask, toolBlockMask, defEntries, examplesMask, defListMask, TAG_ONLY_LINE, stripAnnotation, lineOrdinals, proseExemptLines, configSectionMask, isAnnotationLine, hasTrailingAnnotation, maskQuotes, lower, parseLine, stripInlineComment, splitSentences, predicatesFromProse, displayLine, MODAL_HINT, classifyClause, TRANSCLUSION_LINE_RE, DEF_ENTRY, SNAKE_RE, toolsFromLines, parsePredicate, isAntonym, copulaAlternation, normAtom, canonicalActionKey, TAG_NAME_SRC } from './chunk-Y64EAEYB.js';
3
+
4
+ // src/core/highlight.ts
5
+ function classifyValence(raw) {
6
+ const t = raw.trim().replace(/^#{1,6}\s+/, "").replace(/^>\s?/, "").replace(/^(?:[-*+]|\d+\.)\s+/, "").trim().toLowerCase();
7
+ if (/^(never|don'?t|do not|avoid|must not|cannot|no\b)/.test(t)) return "prohibition";
8
+ if (/^(always|must\b|ensure|make sure|be sure|you must|require)/.test(t)) return "directive";
9
+ if (/^(if|when|whenever|unless|in case|should)/.test(t)) return "conditional";
10
+ if (/^(e\.?g\.?\b|for example|example:|input:|output:|for instance)/.test(t)) return "example";
11
+ if (/(respond (with|only|in)|output (a|the|only|in)|return (a|the|only)|format:|json|markdown|use the following format)/.test(
12
+ t
13
+ )) {
14
+ return "format";
15
+ }
16
+ if (/^(you are|you'?re|act as|your role|your job|as an?\b)/.test(t)) return "role";
17
+ return "plain";
18
+ }
19
+ var INLINE_RULES = [
20
+ { re: /^`[^`\n]+`/, cls: "md-code-inline", delim: 1 },
21
+ { re: /^\*\*\*[^*\n]+\*\*\*/, cls: "md-strong-em", delim: 3 },
22
+ { re: /^\*\*[^*\n]+\*\*/, cls: "md-strong", delim: 2 },
23
+ { re: /^~~[^~\n]+~~/, cls: "md-strike", delim: 2 },
24
+ { re: /^\*[^*\s][^*\n]*\*/, cls: "md-em", delim: 1 }
25
+ ];
26
+ var LINK_RE = /^\[([^\]\n]+)\]\(([^)\n]+)\)/;
27
+ function tokenizeInline(text) {
28
+ const tokens2 = [];
29
+ let buf = "";
30
+ const flush = () => {
31
+ if (buf) {
32
+ tokens2.push({ text: buf });
33
+ buf = "";
34
+ }
35
+ };
36
+ let i = 0;
37
+ while (i < text.length) {
38
+ const sub = text.slice(i);
39
+ const lm = sub.match(LINK_RE);
40
+ if (lm) {
41
+ flush();
42
+ tokens2.push({ text: "[", cls: "md-marker" });
43
+ tokens2.push({ text: lm[1] ?? "", cls: "md-link" });
44
+ tokens2.push({ text: "](", cls: "md-marker" });
45
+ tokens2.push({ text: lm[2] ?? "", cls: "md-link-url" });
46
+ tokens2.push({ text: ")", cls: "md-marker" });
47
+ i += lm[0].length;
48
+ continue;
49
+ }
50
+ let matched = false;
51
+ for (const r of INLINE_RULES) {
52
+ const m = sub.match(r.re);
53
+ if (m) {
54
+ flush();
55
+ const full = m[0];
56
+ tokens2.push({ text: full.slice(0, r.delim), cls: "md-marker" });
57
+ tokens2.push({ text: full.slice(r.delim, full.length - r.delim), cls: r.cls });
58
+ tokens2.push({ text: full.slice(full.length - r.delim), cls: "md-marker" });
59
+ i += full.length;
60
+ matched = true;
61
+ break;
62
+ }
63
+ }
64
+ if (!matched) {
65
+ buf += text[i] ?? "";
66
+ i += 1;
67
+ }
68
+ }
69
+ flush();
70
+ return tokens2;
71
+ }
72
+ function lineLevel(raw) {
73
+ const h = raw.match(/^\s*#{1,6}\s+/);
74
+ if (h) return { lineClass: "md-heading", contentStart: h[0].length };
75
+ const q = raw.match(/^\s*>\s?/);
76
+ if (q) return { lineClass: "md-quote", contentStart: q[0].length };
77
+ const li = raw.match(/^\s*(?:[-*+]|\d+\.)\s+/);
78
+ if (li) return { contentStart: li[0].length };
79
+ return { contentStart: 0 };
80
+ }
81
+ function renderLines(rawLines) {
82
+ const kinds = classifyLines(rawLines);
83
+ const out = [];
84
+ let inFence = false;
85
+ for (let i = 0; i < rawLines.length; i += 1) {
86
+ const raw = rawLines[i] ?? "";
87
+ const kind = kinds[i] ?? "content";
88
+ const t = raw.trim();
89
+ const isFence = t.startsWith("```") || t.startsWith("~~~");
90
+ if (inFence) {
91
+ if (isFence) inFence = false;
92
+ out.push({ kind, lineClass: "md-code", valence: "plain", tokens: [{ text: raw }] });
93
+ continue;
94
+ }
95
+ if (kind === "comment") {
96
+ out.push({ kind, lineClass: "ol-comment", valence: "plain", tokens: [{ text: raw }] });
97
+ continue;
98
+ }
99
+ if (isFence) {
100
+ inFence = true;
101
+ out.push({ kind, lineClass: "md-code", valence: "plain", tokens: [{ text: raw }] });
102
+ continue;
103
+ }
104
+ if (kind === "blank") {
105
+ out.push({ kind, valence: "plain", tokens: [{ text: raw }] });
106
+ continue;
107
+ }
108
+ const { lineClass, contentStart } = lineLevel(raw);
109
+ const marker = contentStart > 0 ? [{ text: raw.slice(0, contentStart), cls: "md-marker" }] : [];
110
+ out.push({
111
+ kind,
112
+ lineClass,
113
+ valence: classifyValence(raw),
114
+ tokens: [...marker, ...tokenizeInline(raw.slice(contentStart))]
115
+ });
116
+ }
117
+ return out;
118
+ }
119
+
120
+ // src/core/fmt.ts
121
+ var INDENT_UNIT = 2;
122
+ new RegExp(`;\\s*(?:and\\s+|but\\s+)?(${MODAL_KEYWORDS.join("|")})\\b`, "i");
123
+ function wrappedContinuations(rawLines) {
124
+ const render = renderLines(rawLines);
125
+ const exempt = proseExemptLines(rawLines);
126
+ const tests = testBlockLines(rawLines);
127
+ const tools = toolBlockMask(rawLines);
128
+ const cfg = configSectionMask(rawLines);
129
+ const out = /* @__PURE__ */ new Map();
130
+ let prevContent = -1;
131
+ for (let i = 0; i < rawLines.length; i += 1) {
132
+ const raw = rawLines[i] ?? "";
133
+ if (tests.has(i)) continue;
134
+ if (tools[i] || cfg[i]) {
135
+ prevContent = -1;
136
+ continue;
137
+ }
138
+ const r = render[i];
139
+ if (!r || r.kind !== "content" || r.lineClass === "md-code") continue;
140
+ if (r.lineClass === "md-heading") {
141
+ prevContent = -1;
142
+ continue;
143
+ }
144
+ const before = prevContent;
145
+ prevContent = i;
146
+ if (before === -1 || exempt.has(i) || exempt.has(before)) continue;
147
+ const parsed = parseLine(raw);
148
+ if (parsed.list !== "none") continue;
149
+ const clause = parsed.clause.trim();
150
+ if (clause === "" || !/^[a-z]/.test(clause)) continue;
151
+ if (classifyClause(clause) !== "prose") continue;
152
+ const prevText = (rawLines[before] ?? "").trim();
153
+ if (prevText === "" || /[.!?:;"'`)\]]$/.test(prevText)) continue;
154
+ out.set(i, before);
155
+ }
156
+ return out;
157
+ }
158
+
159
+ // src/core/layout.ts
160
+ function layoutColumns(rawLines) {
161
+ const n = rawLines.length;
162
+ const col = new Array(n).fill(null);
163
+ const substep = new Array(n).fill(false);
164
+ const render = renderLines(rawLines);
165
+ const zoneMask = illustrationZoneMask(rawLines);
166
+ const toolMask = toolBlockMask(rawLines);
167
+ const swMask = switchBlockMask(rawLines);
168
+ const chainStarts = new Set(parseSwitches(rawLines).filter((b) => b.error === null).map((b) => b.headerLine));
169
+ const testLines = testBlockLines(rawLines);
170
+ const exampleLines = exampleBlockLines(rawLines);
171
+ const doc = buildStructure(rawLines);
172
+ const baseCol = new Array(n).fill(0);
173
+ const tagCol = /* @__PURE__ */ new Map();
174
+ for (const s of doc.sections) {
175
+ if (s.syntax !== "xml") continue;
176
+ const end = Math.min(s.endLine ?? n, n);
177
+ tagCol.set(s.startLine, 2 * s.depth);
178
+ const closeLine = end - 1;
179
+ if (closeLine > s.startLine && matchSectionTag(rawLines[closeLine] ?? "") !== null) {
180
+ tagCol.set(closeLine, 2 * s.depth);
181
+ }
182
+ for (let i = s.startLine + 1; i < closeLine; i += 1) {
183
+ baseCol[i] = Math.max(baseCol[i] ?? 0, 2 * (s.depth + 1));
184
+ }
185
+ }
186
+ const defBody = /* @__PURE__ */ new Map();
187
+ for (const e of defEntries(rawLines)) {
188
+ let fence = false;
189
+ const stack = [];
190
+ let curCol = e.indent + 2;
191
+ for (const li of e.lines) {
192
+ if (li === e.line) continue;
193
+ const raw = rawLines[li] ?? "";
194
+ const t = raw.trim();
195
+ if (t.startsWith("```") || t.startsWith("~~~")) {
196
+ defBody.set(li, fence ? null : curCol);
197
+ fence = !fence;
198
+ continue;
199
+ }
200
+ if (fence || t === "") {
201
+ defBody.set(li, null);
202
+ continue;
203
+ }
204
+ if (t.startsWith("- ")) {
205
+ const si = raw.length - raw.trimStart().length;
206
+ while (stack.length > 0 && (stack[stack.length - 1] ?? 0) >= si) stack.pop();
207
+ stack.push(si);
208
+ curCol = e.indent + 2 * stack.length;
209
+ defBody.set(li, curCol);
210
+ continue;
211
+ }
212
+ defBody.set(li, null);
213
+ }
214
+ }
215
+ for (let i = 0; i < n; i += 1) {
216
+ const raw = rawLines[i] ?? "";
217
+ const t = raw.trim();
218
+ if (t === "") continue;
219
+ if (render[i]?.lineClass === "md-code") continue;
220
+ if (defBody.has(i)) {
221
+ col[i] = defBody.get(i) ?? null;
222
+ continue;
223
+ }
224
+ if (testLines.has(i)) continue;
225
+ if (exampleLines.has(i)) continue;
226
+ if (zoneMask[i] === true && !tagCol.has(i)) continue;
227
+ if (toolMask[i] === true && !/^\s*\$(?:TOOL|SERVICE)\b/i.test(raw)) continue;
228
+ if (swMask[i] === true && !chainStarts.has(i)) continue;
229
+ if (t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
230
+ const leadWs = (raw.match(/^[ \t]*/) ?? [""])[0];
231
+ if (leadWs.includes(" ")) continue;
232
+ col[i] = tagCol.get(i) ?? baseCol[i] ?? 0;
233
+ substep[i] = /^(?:[a-z]\.|\d+(?:\.\d+)+\.?)\s/i.test(t);
234
+ }
235
+ return { col, substep };
236
+ }
237
+
238
+ // src/core/charset.ts
239
+ var BOUND = Object.fromEntries(
240
+ COMPARISON_SYMBOLS.flatMap((c) => c.bound ? c.symbols.map((s) => [s, c.bound]) : [])
241
+ );
242
+ var CHARSET_RULES = [
243
+ // Emoji first, as full ZWJ sequences, so a joiner inside one is never re-flagged as an invisible.
244
+ // ©®™ are Extended_Pictographic but meaning-bearing marks, not decoration — excluded.
245
+ {
246
+ re: /(?![©®™])\p{Extended_Pictographic}(?:️)?(?:‍(?![©®™])\p{Extended_Pictographic}(?:️)?)*/gu,
247
+ source: "structure/special-character",
248
+ msg: "Emoji is outside the language: the instruction plane is checkable English, and models mirror prompt decoration into output. Remove it, or demonstrate it inside <examples>."
249
+ },
250
+ // En-dash between digits is a RANGE — the fix is a hyphen, not a comma.
251
+ {
252
+ re: /(?<=\d) ?– ?(?=\d)/g,
253
+ source: "structure/em-dash",
254
+ msg: "Typographic dash in a range: write a plain hyphen (3-5). The en-dash is a lookalike the parsers do not read.",
255
+ fix: () => ({ title: '\u2192 "-"', replacement: "-" })
256
+ },
257
+ {
258
+ re: / ?[—–] ?/g,
259
+ source: "structure/em-dash",
260
+ msg: "An em-dash splices two statements into one line: the checker reads one unparseable blob, and models mirror the tic (the canonical AI-tell). Split the statements, or use a comma or colon.",
261
+ fix: (m, text) => (m.index ?? 0) > 0 && (m.index ?? 0) + m[0].length < text.length ? { title: '\u2192 ","', replacement: ", " } : null
262
+ },
263
+ {
264
+ re: /[“”„]/g,
265
+ source: "structure/curly-quote",
266
+ msg: 'A curly quote is not a TG string literal: only straight " binds an utterance, so this text is invisible to the quote doctrine. Straighten it.',
267
+ fix: () => ({ title: '\u2192 "', replacement: '"' })
268
+ },
269
+ {
270
+ re: /[‘’‚]/g,
271
+ source: "structure/curly-quote",
272
+ msg: "A curly apostrophe is a lookalike: the parsers read ASCII ', so possession and contractions written with it are invisible to the consistency checker. Straighten it.",
273
+ fix: () => ({ title: "\u2192 '", replacement: "'" })
274
+ },
275
+ {
276
+ re: /≤ ?/g,
277
+ source: "structure/special-character",
278
+ msg: `The numeric tier parses bounds written in English: '\u2264 3' lowers with no bound while '${BOUND["\u2264"]} 3' is Z3-provable. Write it out.`,
279
+ fix: () => ({ title: `\u2192 "${BOUND["\u2264"]}"`, replacement: `${BOUND["\u2264"]} ` })
280
+ },
281
+ {
282
+ re: /≥ ?/g,
283
+ source: "structure/special-character",
284
+ msg: `The numeric tier parses bounds written in English: '\u2265 3' lowers with no bound while '${BOUND["\u2265"]} 3' is Z3-provable. Write it out.`,
285
+ fix: () => ({ title: `\u2192 "${BOUND["\u2265"]}"`, replacement: `${BOUND["\u2265"]} ` })
286
+ },
287
+ {
288
+ re: / ?≠ ?/g,
289
+ source: "structure/special-character",
290
+ msg: "The parsers read negation in English: write 'is not'.",
291
+ fix: () => ({ title: '\u2192 "is not"', replacement: " is not " })
292
+ },
293
+ {
294
+ re: /−/g,
295
+ // U+2212 minus sign, a hyphen lookalike
296
+ source: "structure/special-character",
297
+ msg: 'Unicode minus sign: a hyphen lookalike the parsers do not read. Use "-".',
298
+ fix: () => ({ title: '\u2192 "-"', replacement: "-" })
299
+ },
300
+ {
301
+ re: /(?<=\d) ?× ?(?=\d)/g,
302
+ source: "structure/special-character",
303
+ msg: 'Multiplication sign: write a plain "x" (3x) \u2014 the \xD7 is a lookalike outside the language.',
304
+ fix: () => ({ title: '\u2192 "x"', replacement: "x" })
305
+ },
306
+ {
307
+ re: /…/g,
308
+ source: "structure/special-character",
309
+ msg: 'Unicode ellipsis: write "..." \u2014 or finish the statement.',
310
+ fix: () => ({ title: '\u2192 "..."', replacement: "..." })
311
+ },
312
+ {
313
+ re: /(?<=^\s*)[•◦▪‣·] ?/g,
314
+ source: "structure/special-character",
315
+ msg: 'Not a list bullet the parser reads: use "- ".',
316
+ fix: () => ({ title: '\u2192 "- "', replacement: "- " })
317
+ },
318
+ {
319
+ re: /[→←⇒⇐↔⇔↦➜➤▶◀]/g,
320
+ source: "structure/special-character",
321
+ msg: 'An arrow encodes sequence or mapping the language already has words for: use THEN for flow, "to" for mappings. Arrows belong only inside <examples> demos.'
322
+ },
323
+ {
324
+ re: /[   ]/g,
325
+ // no-break space family — invisible, breaks token/word matching
326
+ source: "structure/special-character",
327
+ msg: "Non-breaking space: an invisible lookalike that breaks word matching and is a known injection surface. Use a plain space.",
328
+ fix: () => ({ title: "\u2192 space", replacement: " " })
329
+ },
330
+ {
331
+ re: /[​‌‍⁠️]/g,
332
+ // zero-width family — pure sabotage
333
+ source: "structure/special-character",
334
+ msg: "Zero-width character: invisible to a human, real to the model \u2014 a documented injection vector. Remove it.",
335
+ fix: () => ({ title: "Remove", replacement: "" })
336
+ },
337
+ {
338
+ re: /[─-╿]/g,
339
+ // box drawing
340
+ source: "structure/special-character",
341
+ msg: 'Box-drawing character: verbatim layouts belong in a <"name"> literal zone or a fence, not the instruction plane.'
342
+ },
343
+ {
344
+ re: /[•◦▪‣·★☆✦✧✱✳✴❇✨†‡¶≈×÷±∞]/g,
345
+ source: "structure/special-character",
346
+ msg: "Decorative or symbolic Unicode is outside the language: every mark here has an English or ASCII form the checker reads. Use plain text; illustrations belong in <examples>."
347
+ }
348
+ ];
349
+ var SPECIAL_ANY = /[—–“”„‘’‚…•◦▪‣·≥≤≠≈×÷±∞−★☆✦✧✱✳✴❇✨†‡¶→←⇒⇐↔⇔↦➜➤▶◀   ​-‍⁠️─-╿]|\p{Extended_Pictographic}/u;
350
+ var CHARSET_DOC = {
351
+ id: "charset",
352
+ title: "Character set",
353
+ lead: 'The instruction plane is plain ASCII: typographic and decorative codepoints are **compile errors**, each with a one-click ASCII fix. Not aesthetics \u2014 every mark here hides meaning from a tier that parses the ASCII/English form (a curly apostrophe conceals a possession contradiction; `\u2264 3` lowers with no bound while `at most 3` is Z3-provable; an em-dash splices two statements into one line). Symbols are legal only inside the literal zones: `<examples>`, `<"name">`, fences, and `"quotes"`.',
354
+ headers: ["Not allowed", "Fix", "Why"],
355
+ rows: [
356
+ ["`\u2014` `\u2013` em/en dash", "`,` or `:`", "Splices two statements into one line; the canonical AI-tell, mirrored into output"],
357
+ ["`\u201C \u201D` `\u2018 \u2019` curly quotes", "straighten to ASCII", "Only a straight quote binds a string literal; a curly apostrophe hides a possession contradiction"],
358
+ ["`\u2264` `\u2265` `\u2260`", `\`${BOUND["\u2264"]}\` \`${BOUND["\u2265"]}\` \`is not\``, "A bound in English is Z3-provable; the symbol lowers with no bound"],
359
+ ["`\u2026` `\u2212` `\xD7`", "`...` `-` `x`", "Lookalikes the parsers do not read"],
360
+ ["`\u2022` `\u25E6` `\u25AA` bullets", "`- `", "Not a list bullet the parser reads"],
361
+ ["`\u2192` `\u21D2` `\u2194` arrows", "`THEN` or the word `to`", "The language already has words for flow and mapping"],
362
+ ["non-breaking / zero-width space", "a plain space, or remove", "Invisible saboteurs and a documented injection surface"],
363
+ ["box-drawing, `\u2605 \xF7 \xB1 \xB6`, emoji", 'a fence or `<"name">` zone', "Verbatim layout and decoration belong in the literal zones"]
364
+ ]
365
+ };
366
+
367
+ // src/core/logic.ts
368
+ var TRUE = { t: "true" };
369
+ function atomsOf(f, into) {
370
+ if (f.t === "atom") into.add(f.name);
371
+ else if (f.t === "not") atomsOf(f.f, into);
372
+ else if (f.t === "and" || f.t === "or") f.fs.forEach((x) => atomsOf(x, into));
373
+ }
374
+ function evalF(f, a) {
375
+ switch (f.t) {
376
+ case "true":
377
+ return true;
378
+ case "false":
379
+ return false;
380
+ case "atom":
381
+ return a.get(f.name) ?? false;
382
+ case "not":
383
+ return !evalF(f.f, a);
384
+ case "and":
385
+ return f.fs.every((x) => evalF(x, a));
386
+ case "or":
387
+ return f.fs.some((x) => evalF(x, a));
388
+ }
389
+ }
390
+ function satisfiable(f) {
391
+ const set = /* @__PURE__ */ new Set();
392
+ atomsOf(f, set);
393
+ const atoms = [...set];
394
+ const n = atoms.length;
395
+ if (n > 20) return true;
396
+ for (let mask = 0; mask < 1 << n; mask += 1) {
397
+ const a = /* @__PURE__ */ new Map();
398
+ for (let i = 0; i < n; i += 1) a.set(atoms[i] ?? "", (mask & 1 << i) !== 0);
399
+ if (evalF(f, a)) return true;
400
+ }
401
+ return false;
402
+ }
403
+ var PROHIBIT = ["MUST NOT", "DO NOT", "DON'T", "NEVER"];
404
+ var OBLIGATE = ["NEEDS TO", "NEED TO", "HAVE TO", "HAS TO", "MUST", "ALWAYS", "REQUIRE", "ENSURE"];
405
+ function foldLeadingNegation(kind, action) {
406
+ let k = kind;
407
+ let a = action.trim();
408
+ for (; ; ) {
409
+ const m = /^(?:not|never)\s+/i.exec(a);
410
+ if (!m) return { kind: k, action: a };
411
+ k = k === "obligate" ? "prohibit" : "obligate";
412
+ a = a.slice(m[0].length);
413
+ }
414
+ }
415
+ function negate(f) {
416
+ if (f.t === "true") return { t: "false" };
417
+ if (f.t === "false") return TRUE;
418
+ return { t: "not", f };
419
+ }
420
+ function implies(a, b) {
421
+ return !satisfiable({ t: "and", fs: [a, negate(b)] });
422
+ }
423
+ function guardsCollide(c1, c2) {
424
+ return implies(c1, c2) || implies(c2, c1);
425
+ }
426
+ function parseCondition(text) {
427
+ const t = text.trim();
428
+ if (t === "") return TRUE;
429
+ const ors = t.split(/\s+OR\s+/i).map((op) => {
430
+ const ands = op.split(/\s+AND\s+/i).map((ap) => ap.trim()).filter((x) => x !== "").map((term) => {
431
+ const neg = /^NOT\s+/i.test(term);
432
+ const atom = { t: "atom", name: normAtom(neg ? term.replace(/^NOT\s+/i, "") : term) };
433
+ return neg ? { t: "not", f: atom } : atom;
434
+ });
435
+ return ands.length === 0 ? TRUE : ands.length === 1 ? ands[0] ?? TRUE : { t: "and", fs: ands };
436
+ });
437
+ return ors.length === 1 ? ors[0] ?? TRUE : { t: "or", fs: ors };
438
+ }
439
+ var FREQ_KIND_TO_FREQ = { existential: "some", majority: "most", minority: "few" };
440
+ var FREQ_ALT = FREQUENCY_WORDS.flatMap((f) => f.words.map((w) => ({ w, freq: FREQ_KIND_TO_FREQ[f.kind] }))).sort((a, b) => b.w.length - a.w.length);
441
+ var FREQ_LEAD_RE = new RegExp(`^(?:you\\s+)?(${FREQ_ALT.map((x) => x.w.replace(/ /g, "\\s+")).join("|")})\\b,?\\s+(.+)$`, "i");
442
+ function peelFrequency(text) {
443
+ const m = text.trim().match(FREQ_LEAD_RE);
444
+ if (!m) return { freq: null, rest: text };
445
+ const word = (m[1] ?? "").toUpperCase().replace(/\s+/g, " ");
446
+ const hit = FREQ_ALT.find((x) => x.w === word);
447
+ return hit ? { freq: hit.freq, rest: m[2] ?? "" } : { freq: null, rest: text };
448
+ }
449
+ function pInterval(kind, freq) {
450
+ if (kind === "obligate") {
451
+ if (freq === "all") return { lo: 1, hi: 1, loOpen: false, hiOpen: false };
452
+ if (freq === "most") return { lo: 0.5, hi: 1, loOpen: true, hiOpen: false };
453
+ if (freq === "few") return { lo: 0, hi: 0.5, loOpen: true, hiOpen: true };
454
+ return { lo: 0, hi: 1, loOpen: true, hiOpen: false };
455
+ }
456
+ if (freq === "all") return { lo: 0, hi: 0, loOpen: false, hiOpen: false };
457
+ if (freq === "most") return { lo: 0, hi: 0.5, loOpen: false, hiOpen: true };
458
+ if (freq === "few") return { lo: 0.5, hi: 1, loOpen: true, hiOpen: true };
459
+ return { lo: 0, hi: 1, loOpen: false, hiOpen: true };
460
+ }
461
+ var disjointP = (a, b) => {
462
+ if (a.hi < b.lo || b.hi < a.lo) return true;
463
+ if (a.hi === b.lo) return a.hiOpen || b.loOpen;
464
+ if (b.hi === a.lo) return b.hiOpen || a.loOpen;
465
+ return false;
466
+ };
467
+ var subsetP = (a, b) => {
468
+ const loOk = a.lo > b.lo || a.lo === b.lo && (b.loOpen ? a.loOpen : true);
469
+ const hiOk = a.hi < b.hi || a.hi === b.hi && (b.hiOpen ? a.hiOpen : true);
470
+ return loOk && hiOk;
471
+ };
472
+ var FREQ_PHRASE = { all: "on every occasion", most: "on most occasions", some: "on some occasions", few: "on few occasions" };
473
+ var freqPhrase = (c) => `${c.kind === "obligate" ? "required" : "skipped"} ${FREQ_PHRASE[c.freq]}`;
474
+ function keyedClause(kind, action, scope, tools) {
475
+ const { key, parsed, canonical } = canonicalActionKey(action, tools);
476
+ let k = kind;
477
+ let viaAntonym = false;
478
+ if (canonical && parsed && parsed.lexicalPolarity === -1) {
479
+ k = k === "obligate" ? "prohibit" : "obligate";
480
+ viaAntonym = true;
481
+ }
482
+ return { kind: k, atom: scope + key, display: action.trim(), canonical, viaAntonym };
483
+ }
484
+ function matchModalAt(rest, subject, tools, addressees) {
485
+ const canon = subject !== "you" && addressees?.has(subject) === true ? "you" : subject;
486
+ const scope = canon === "you" ? "" : `${canon}: `;
487
+ const u = rest.toUpperCase();
488
+ const graded = (kind0, raw) => {
489
+ const pre = peelFrequency(raw);
490
+ const f = foldLeadingNegation(kind0, pre.rest);
491
+ const post = peelFrequency(f.action);
492
+ return { ...keyedClause(f.kind, post.rest, scope, tools), freq: pre.freq ?? post.freq ?? "all" };
493
+ };
494
+ for (const p of PROHIBIT) {
495
+ if (u.startsWith(`${p} `)) return graded("prohibit", rest.slice(p.length));
496
+ }
497
+ for (const o of OBLIGATE) {
498
+ if (u.startsWith(`${o} `)) return graded("obligate", rest.slice(o.length));
499
+ }
500
+ return null;
501
+ }
502
+ function detectModal(seg, tools, addressees) {
503
+ const s = seg.trim();
504
+ const bare = matchModalAt(s, "you", tools, addressees);
505
+ if (bare) return bare;
506
+ const words = s.split(/\s+/);
507
+ for (let i = 1; i < words.length; i += 1) {
508
+ const subject = words.slice(0, i).join(" ").replace(/^(?:the|a|an)\s+/i, "").trim().toLowerCase();
509
+ const m = matchModalAt(words.slice(i).join(" "), subject === "" ? "you" : subject, tools, addressees);
510
+ if (m) return m;
511
+ }
512
+ return null;
513
+ }
514
+ function splitGuard(sentence) {
515
+ const s = sentence.trim();
516
+ if (!/^IF\b/i.test(s)) return null;
517
+ const rest = s.slice(2);
518
+ for (let i = rest.indexOf(","); i !== -1; i = rest.indexOf(",", i + 1)) {
519
+ const body = rest.slice(i + 1).trim().replace(/^(?:you\s+then\s+|then\s+you\s+)/i, "you ").replace(/^then\s+/i, "");
520
+ if (body !== "" && stripDeonticLead(body).matched) return { cond: rest.slice(0, i), body };
521
+ }
522
+ const thenSplit = s.match(/^IF\b(.+?)\bTHEN\b(.+)$/i);
523
+ if (thenSplit) return { cond: thenSplit[1] ?? "", body: (thenSplit[2] ?? "").trim() };
524
+ return null;
525
+ }
526
+ function stripDeonticLead(segment, opts) {
527
+ const direct = (text, subject) => {
528
+ const u = text.toUpperCase();
529
+ for (const p of PROHIBIT) if (u.startsWith(`${p} `)) return { body: text.slice(p.length + 1), prohibited: true, matched: true, subject };
530
+ for (const o of OBLIGATE) {
531
+ if (u.startsWith(`${o} `)) {
532
+ const rest = text.slice(o.length + 1);
533
+ const neg = rest.match(/^not\s+/i);
534
+ return neg ? { body: rest.slice(neg[0].length), prohibited: true, matched: true, subject } : { body: rest, prohibited: false, matched: true, subject };
535
+ }
536
+ }
537
+ return null;
538
+ };
539
+ const s = segment.trim().replace(/^you\s+/i, "");
540
+ const hit = direct(s, "you");
541
+ if (hit) return hit;
542
+ if (opts?.peelSubject === true) {
543
+ const words = s.split(/\s+/);
544
+ for (let i = 1; i <= 3 && i < words.length; i += 1) {
545
+ const raw = words.slice(0, i);
546
+ if (!raw.every((w) => /^[A-Za-z][A-Za-z'-]*$/.test(w) && !/^(?:if|then|otherwise|and|or|when|while|unless)$/i.test(w))) break;
547
+ const m = direct(words.slice(i).join(" "), raw.join(" ").toLowerCase().replace(/^(?:the|a|an)\s+/, "") || "you");
548
+ if (m) return m;
549
+ }
550
+ }
551
+ return { body: s, prohibited: false, matched: false, subject: "you" };
552
+ }
553
+ function pushSegments(text, cond, line, out, tools, imperativesAllowed = false, addressees) {
554
+ for (const seg of text.split(";")) {
555
+ const lead = peelFrequency(seg);
556
+ const core = lead.freq === null ? seg : lead.rest;
557
+ const m = detectModal(core, tools, addressees);
558
+ if (m && m.atom) {
559
+ out.push({ ...m, freq: lead.freq ?? m.freq, cond, line });
560
+ continue;
561
+ }
562
+ if (!imperativesAllowed || false) continue;
563
+ const t = core.trim();
564
+ if (t === "") continue;
565
+ const parsed = parsePredicate(t, tools);
566
+ if (parsed && (parsed.confidence === "lexicon" || lead.freq !== null)) {
567
+ const clause = keyedClause("obligate", t.replace(/[.!?]+$/, ""), "", tools);
568
+ out.push({ ...clause, freq: lead.freq ?? "all", cond, line });
569
+ }
570
+ }
571
+ }
572
+ function extractClauses(rawLine, line, tools, exempt, addressees) {
573
+ const imperativesAllowed = exempt === void 0 ? false : !exempt.has(line);
574
+ const { clause } = parseLine(rawLine);
575
+ const masked = maskQuotes(clause);
576
+ const out = [];
577
+ const sentences = splitSentences(masked);
578
+ const segments = sentences.length > 1 ? sentences.map((s) => s.text) : [masked];
579
+ for (const sentence of segments) {
580
+ let cond = TRUE;
581
+ let body = sentence;
582
+ const g = splitGuard(sentence);
583
+ if (g) {
584
+ cond = parseCondition(g.cond);
585
+ body = g.body;
586
+ }
587
+ const ow = g ? body.match(/^(.+?)\bOTHERWISE\b(.+)$/i) : null;
588
+ if (ow) {
589
+ pushSegments(ow[1] ?? "", cond, line, out, tools, imperativesAllowed, addressees);
590
+ pushSegments(ow[2] ?? "", negate(cond), line, out, tools, imperativesAllowed, addressees);
591
+ } else {
592
+ pushSegments(body, cond, line, out, tools, imperativesAllowed, addressees);
593
+ }
594
+ }
595
+ return out;
596
+ }
597
+ function checkConsistency(lines, addressees) {
598
+ const tools = toolsFromLines(lines);
599
+ const exempt = proseExemptLines(lines);
600
+ const clauses = [];
601
+ lines.forEach((l, i) => clauses.push(...extractClauses(l, i, tools, exempt, addressees)));
602
+ const byAtom = /* @__PURE__ */ new Map();
603
+ for (const c of clauses) {
604
+ const list = byAtom.get(c.atom);
605
+ if (list) list.push(c);
606
+ else byAtom.set(c.atom, [c]);
607
+ }
608
+ const conflicts = [];
609
+ const seen = /* @__PURE__ */ new Set();
610
+ for (const [atom, cs] of byAtom) {
611
+ for (let i = 0; i < cs.length; i += 1) {
612
+ for (let j = i + 1; j < cs.length; j += 1) {
613
+ const a = cs[i];
614
+ const b = cs[j];
615
+ const ia = pInterval(a.kind, a.freq);
616
+ const ib = pInterval(b.kind, b.freq);
617
+ const hard = disjointP(ia, ib);
618
+ const equal = !hard && subsetP(ia, ib) && subsetP(ib, ia);
619
+ const strong = hard || equal ? null : subsetP(ia, ib) ? a : subsetP(ib, ia) ? b : null;
620
+ if (!hard && strong === null) continue;
621
+ if (!guardsCollide(a.cond, b.cond)) continue;
622
+ const conflictLines = [.../* @__PURE__ */ new Set([a.line, b.line])].sort((x, y) => x - y);
623
+ const key = `${hard ? "hard" : "strength"}|${atom}|${conflictLines.join(",")}`;
624
+ if (seen.has(key)) continue;
625
+ seen.add(key);
626
+ const viaAntonym = a.viaAntonym || b.viaAntonym;
627
+ if (hard) {
628
+ const o = a.kind === "obligate" ? a : b;
629
+ const p2 = o === a ? b : a;
630
+ const name = o.canonical || p2.canonical ? o.display === p2.display ? o.display : `${o.display}" vs "${p2.display}` : atom;
631
+ conflicts.push({
632
+ atom,
633
+ lines: conflictLines,
634
+ viaAntonym,
635
+ message: a.freq === "all" && b.freq === "all" ? `Logical conflict \u2014 "${name}" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).` : `Frequency conflict \u2014 "${name}" is ${freqPhrase(o)} but ${freqPhrase(p2)}. The occasions cannot both hold.`
636
+ });
637
+ } else if (strong !== null) {
638
+ const weak = strong === a ? b : a;
639
+ conflicts.push({
640
+ atom,
641
+ lines: conflictLines,
642
+ viaAntonym,
643
+ strength: true,
644
+ message: `One rule, two strengths \u2014 "${strong.display}" (${freqPhrase(strong)}) already entails "${weak.display}" (${freqPhrase(weak)}); a model may read the weaker as "not always". Keep the stronger line.`
645
+ });
646
+ }
647
+ }
648
+ }
649
+ }
650
+ return conflicts;
651
+ }
652
+ function checkRedundancy(lines, addressees) {
653
+ const tools = toolsFromLines(lines);
654
+ const exempt = proseExemptLines(lines);
655
+ const clauses = [];
656
+ lines.forEach((l, i) => clauses.push(...extractClauses(l, i, tools, exempt, addressees)));
657
+ const byKey = /* @__PURE__ */ new Map();
658
+ for (const c of clauses) {
659
+ const key = `${c.kind}|${c.freq}|${c.atom}`;
660
+ const list = byKey.get(key);
661
+ if (list) list.push(c);
662
+ else byKey.set(key, [c]);
663
+ }
664
+ const groups = [];
665
+ for (const cs of byKey.values()) {
666
+ if (cs.length < 2) continue;
667
+ const used = new Array(cs.length).fill(false);
668
+ for (let a = 0; a < cs.length; a += 1) {
669
+ const ca = cs[a];
670
+ if (used[a] || !ca) continue;
671
+ used[a] = true;
672
+ const cluster = [ca];
673
+ for (let b = a + 1; b < cs.length; b += 1) {
674
+ const cb = cs[b];
675
+ if (used[b] || !cb) continue;
676
+ if (implies(ca.cond, cb.cond) && implies(cb.cond, ca.cond)) {
677
+ used[b] = true;
678
+ cluster.push(cb);
679
+ }
680
+ }
681
+ const distinct = [...new Set(cluster.map((c) => c.line))].sort((x, y) => x - y);
682
+ if (distinct.length >= 2) groups.push({ atom: ca.canonical ? ca.display : ca.atom, display: ca.display, kind: ca.kind, lines: distinct });
683
+ }
684
+ }
685
+ return groups;
686
+ }
687
+ function checkSubsumption(lines, addressees) {
688
+ const tools = toolsFromLines(lines);
689
+ const exempt = proseExemptLines(lines);
690
+ const clauses = [];
691
+ lines.forEach((l, i) => clauses.push(...extractClauses(l, i, tools, exempt, addressees)));
692
+ const byKey = /* @__PURE__ */ new Map();
693
+ for (const c of clauses) {
694
+ const key = `${c.kind}|${c.freq}|${c.atom}`;
695
+ const list = byKey.get(key);
696
+ if (list) list.push(c);
697
+ else byKey.set(key, [c]);
698
+ }
699
+ const out = [];
700
+ const reported = /* @__PURE__ */ new Set();
701
+ for (const cs of byKey.values()) {
702
+ if (cs.length < 2) continue;
703
+ for (const x of cs) {
704
+ if (reported.has(x.line)) continue;
705
+ const cover = cs.find((y) => y.line !== x.line && implies(x.cond, y.cond) && !implies(y.cond, x.cond));
706
+ if (!cover) continue;
707
+ reported.add(x.line);
708
+ out.push({ atom: x.canonical ? x.display : x.atom, kind: x.kind, redundantLine: x.line, coveringLine: cover.line });
709
+ }
710
+ }
711
+ return out;
712
+ }
713
+ function deadRules(rawLines) {
714
+ const dead = [];
715
+ rawLines.forEach((raw, i) => {
716
+ const g = splitGuard(maskQuotes(parseLine(raw).clause));
717
+ if (!g) return;
718
+ const cond = parseCondition(g.cond);
719
+ if (cond.t !== "true" && !satisfiable(cond)) dead.push(i);
720
+ });
721
+ return dead;
722
+ }
723
+
724
+ // src/core/order.ts
725
+ function normNode(s) {
726
+ return s.trim().toLowerCase().replace(/^the\s+/, "").replace(/[.,;:!?]+$/, "").replace(/\s+/g, " ").trim();
727
+ }
728
+ var GREATER = /* @__PURE__ */ new Set([
729
+ "higher",
730
+ "greater",
731
+ "more",
732
+ "larger",
733
+ "bigger",
734
+ "better",
735
+ "faster",
736
+ "stronger",
737
+ "longer",
738
+ "taller",
739
+ "heavier",
740
+ "older",
741
+ "newer",
742
+ "hotter",
743
+ "harder",
744
+ "wider",
745
+ "broader",
746
+ "deeper"
747
+ ]);
748
+ var LESSER = /* @__PURE__ */ new Set([
749
+ "lower",
750
+ "less",
751
+ "smaller",
752
+ "worse",
753
+ "slower",
754
+ "weaker",
755
+ "shorter",
756
+ "younger",
757
+ "lighter",
758
+ "narrower"
759
+ ]);
760
+ function comparativeEdges(clauses) {
761
+ const byDim = /* @__PURE__ */ new Map();
762
+ for (const { clause, line } of clauses) {
763
+ const m = clause.match(/^(.+?)\s+(?:is|are|was|were)\s+(.+?)\s+than\s+(.+)$/i);
764
+ if (!m) continue;
765
+ const a = normNode(m[1] ?? "");
766
+ const b = normNode(m[3] ?? "");
767
+ if (!a || !b) continue;
768
+ const words = (m[2] ?? "").trim().toLowerCase().replace(/^(?:a|an|the)\s+/, "").split(/\s+/);
769
+ const dir = words[0] ?? "";
770
+ let edge = null;
771
+ if (GREATER.has(dir)) edge = { from: a, to: b, line };
772
+ else if (LESSER.has(dir)) edge = { from: b, to: a, line };
773
+ if (!edge) continue;
774
+ const key = words.slice(1).join(" ") || "_ord_";
775
+ if (!byDim.has(key)) byDim.set(key, []);
776
+ byDim.get(key)?.push(edge);
777
+ }
778
+ return byDim;
779
+ }
780
+ function timeValue(token) {
781
+ const named = token.trim().toLowerCase();
782
+ if (named === "noon" || named === "midday") return 12 * 60;
783
+ if (named === "midnight") return 0;
784
+ let m = token.match(/^(\d{1,2})(?::?(\d{2}))?\s*(am|pm)$/i);
785
+ if (m) {
786
+ let h = Number(m[1]) % 12;
787
+ if (/pm/i.test(m[3] ?? "")) h += 12;
788
+ return h * 60 + Number(m[2] || 0);
789
+ }
790
+ m = token.match(/^(\d{1,2}):(\d{2})$/);
791
+ if (m) return Number(m[1]) * 60 + Number(m[2]);
792
+ return null;
793
+ }
794
+ function stemEvent(node) {
795
+ const parts = node.split(" ");
796
+ const w = parts[0] ?? "";
797
+ if (w.length > 5 && w.endsWith("ing")) {
798
+ let base = w.slice(0, -3);
799
+ const last = base[base.length - 1] ?? "";
800
+ if (last !== "" && last === base[base.length - 2] && !"aeiou".includes(last)) base = base.slice(0, -1);
801
+ parts[0] = base;
802
+ }
803
+ return parts.join(" ");
804
+ }
805
+ function temporalEdges(clauses) {
806
+ const edges = [];
807
+ const points = /* @__PURE__ */ new Set();
808
+ for (const { clause, line } of clauses) {
809
+ const m = clause.match(/^(.+?)\s+(before|after|prior to|earlier than|later than)\s+(.+)$/i);
810
+ if (!m) continue;
811
+ const x = stemEvent(normNode(m[1] ?? ""));
812
+ const y = stemEvent(normNode(m[3] ?? ""));
813
+ if (!x || !y) continue;
814
+ const rel = (m[2] ?? "").toLowerCase();
815
+ const before = rel === "before" || rel === "prior to" || rel === "earlier than";
816
+ edges.push(before ? { from: x, to: y, line } : { from: y, to: x, line });
817
+ if (timeValue(x) !== null) points.add(x);
818
+ if (timeValue(y) !== null) points.add(y);
819
+ }
820
+ const sorted = [...points].map((p) => ({ p, v: timeValue(p) ?? 0 })).sort((m, n) => m.v - n.v);
821
+ for (let i = 0; i < sorted.length - 1; i += 1) {
822
+ const lo = sorted[i];
823
+ const hi = sorted[i + 1];
824
+ if (lo && hi && lo.v < hi.v) edges.push({ from: lo.p, to: hi.p, line: -1 });
825
+ }
826
+ return edges;
827
+ }
828
+ function findCycle(edges) {
829
+ const adj = /* @__PURE__ */ new Map();
830
+ for (const e of edges) {
831
+ if (!adj.has(e.from)) adj.set(e.from, []);
832
+ adj.get(e.from)?.push({ to: e.to, line: e.line });
833
+ }
834
+ const color = /* @__PURE__ */ new Map();
835
+ const path = [];
836
+ const dfs = (u) => {
837
+ color.set(u, 1);
838
+ for (const { to, line } of adj.get(u) ?? []) {
839
+ path.push(line);
840
+ if (color.get(to) === 1) return [...new Set(path.filter((l) => l >= 0))].sort((m, n) => m - n);
841
+ if (!color.has(to)) {
842
+ const found = dfs(to);
843
+ if (found) return found;
844
+ }
845
+ path.pop();
846
+ }
847
+ color.set(u, 2);
848
+ return null;
849
+ };
850
+ for (const node of adj.keys()) {
851
+ if (!color.has(node)) {
852
+ path.length = 0;
853
+ const found = dfs(node);
854
+ if (found) return found;
855
+ }
856
+ }
857
+ return null;
858
+ }
859
+ function checkOrder(rawLines) {
860
+ const clauses = rawLines.map((raw, line) => ({ clause: maskQuotes(parseLine(raw).clause), line })).filter((c) => c.clause.trim() !== "");
861
+ const out = [];
862
+ for (const edges of comparativeEdges(clauses).values()) {
863
+ const cycle = findCycle(edges);
864
+ if (cycle && cycle.length > 0) {
865
+ out.push({ lines: cycle, message: "Logical conflict \u2014 these comparative claims (more/better/higher than) chain back onto themselves, so no consistent ranking exists. Remove or reverse one link of the loop." });
866
+ }
867
+ }
868
+ const temporal = findCycle(temporalEdges(clauses));
869
+ if (temporal && temporal.length > 0) {
870
+ out.push({ lines: temporal, message: "Logical conflict \u2014 these before/after claims chain back onto themselves, so no consistent timeline exists. Remove or reverse one link of the loop." });
871
+ }
872
+ return out;
873
+ }
874
+
875
+ // src/core/definiteness.ts
876
+ var KIND_NOUN = { tool: "tool", section: "section", step: "step", variable: "variable", input: "variable", route: "route" };
877
+ var kindLabel = { tool: "tool", section: "section", step: "step", variable: "variable", route: "route" };
878
+ var DEF_BINDING = new RegExp(`\\bthe\\s+([a-z]+)\\s+(?:${copulaAlternation})\\s+([a-z][a-z0-9_-]*)\\b`, "i");
879
+ function checkDefiniteness(rawLines) {
880
+ const ix = buildStructure(rawLines).index;
881
+ const registry = {
882
+ tool: ix.tools,
883
+ section: ix.sections,
884
+ step: ix.steps,
885
+ variable: ix.inputs,
886
+ route: ix.routes
887
+ };
888
+ const kinds = classifyLines(rawLines);
889
+ const litMask = examplesMask(rawLines);
890
+ const bindings = [];
891
+ let inFence = false;
892
+ rawLines.forEach((line, i) => {
893
+ const t = line.trim();
894
+ if (t.startsWith("```") || t.startsWith("~~~")) {
895
+ inFence = !inFence;
896
+ return;
897
+ }
898
+ if (inFence || litMask[i] || kinds[i] === "comment" || isHeadingLine(line)) return;
899
+ const masked = maskQuotes(stripAnnotation(line)).replace(/\s+\/\/.*$/, (m2) => " ".repeat(m2.length));
900
+ const m = masked.match(DEF_BINDING);
901
+ if (!m) return;
902
+ const kind = KIND_NOUN[(m[1] ?? "").toLowerCase()];
903
+ if (!kind) return;
904
+ bindings.push({ kind, value: (m[2] ?? "").toLowerCase(), line: i });
905
+ });
906
+ const out = [];
907
+ const byKind = /* @__PURE__ */ new Map();
908
+ for (const b of bindings) byKind.set(b.kind, [...byKind.get(b.kind) ?? [], b]);
909
+ for (const [kind, bs] of byKind) {
910
+ const distinct = /* @__PURE__ */ new Map();
911
+ for (const b of bs) if (!distinct.has(b.value)) distinct.set(b.value, b.line);
912
+ const label = kindLabel[kind];
913
+ if (distinct.size >= 2) {
914
+ const vals = [...distinct.keys()].map((v) => `"${v}"`).join(", ");
915
+ out.push({
916
+ lines: [...distinct.values()].sort((a, b) => a - b),
917
+ code: "definite-ambiguous",
918
+ message: `Ambiguous definite reference \u2014 "the ${label}" is bound to ${distinct.size} different ${label}s (${vals}). A definite "the ${label}" names exactly one \u2014 say which.`
919
+ });
920
+ } else if (registry[kind].size === 0) {
921
+ const [line] = [...distinct.values()];
922
+ out.push({
923
+ lines: [line ?? bs[0]?.line ?? 0],
924
+ code: "definite-undeclared",
925
+ message: `Dangling definite reference \u2014 "the ${label}" names a ${label}, but no ${label} is declared. Declare it, or drop "the".`
926
+ });
927
+ }
928
+ }
929
+ return out;
930
+ }
931
+
932
+ // src/core/quantifier.ts
933
+ var QUANT_CONTRARY_FIRES = true;
934
+ var KIND_OF = new Map(QUANTIFIER_WORDS.flatMap((q) => q.words.map((w) => [w.toLowerCase(), q.kind])));
935
+ var SUBJECT_QUANT_ALT = QUANTIFIER_WORDS.filter((q) => q.kind !== "exclusive").flatMap((q) => q.words).map((w) => w.toLowerCase()).join("|");
936
+ var EXCLUSIVE_ALT = QUANTIFIER_WORDS.filter((q) => q.kind === "exclusive").flatMap((q) => q.words).map((w) => w.toLowerCase()).join("|");
937
+ var EXCLUSIVE_RE = new RegExp(`^(?:${EXCLUSIVE_ALT})\\s+(.+)$`, "i");
938
+ var SUBJECT_QUANT_RE = new RegExp(`^(${SUBJECT_QUANT_ALT})\\s+(?:of\\s+(?:the\\s+)?)?(\\w+)\\s+(.+)$`, "i");
939
+ function corner(kind, negated) {
940
+ switch (kind) {
941
+ case "universal":
942
+ return negated ? "E" : "A";
943
+ case "negative":
944
+ return negated ? "A" : "E";
945
+ case "existential":
946
+ return negated ? "O" : "I";
947
+ case "majority":
948
+ return negated ? "M-" : "M+";
949
+ case "exclusive":
950
+ return null;
951
+ }
952
+ }
953
+ function opposes(a, b) {
954
+ const has = (x, y) => a === x && b === y || a === y && b === x;
955
+ if (has("A", "O") || has("E", "I")) return "hard";
956
+ if (has("M+", "M-") || has("M+", "E") || has("M-", "A")) return "hard";
957
+ if (has("A", "E")) return "contrary";
958
+ return null;
959
+ }
960
+ var normNoun = singularizeNoun;
961
+ function bareProperty(rest) {
962
+ return rest.replace(/^(?:must(?:\s+be)?|should(?:\s+be)?|shall(?:\s+be)?|will(?:\s+be)?|is|are|be|gets?|always)\s+/i, "").trim();
963
+ }
964
+ function parseScope(rest) {
965
+ const strip = (s) => s.replace(/[.,;:!?]+$/, "").trim();
966
+ const neg = rest.match(/\b(?:is\s+not|are\s+not|was\s+not|were\s+not|isn't|wasn't|do\s+not|does\s+not|don't|must\s+not|should\s+not|never|not)\s+(.+)$/i);
967
+ if (neg) return { negated: true, text: strip(bareProperty(neg[1] ?? "")) };
968
+ return { negated: false, text: strip(bareProperty(rest)) };
969
+ }
970
+ function normMember(s) {
971
+ return s.toLowerCase().replace(/^(?:a|an|the)\s+/, "").replace(/[.,;:!?]+$/, "").replace(/\s+/g, " ").trim();
972
+ }
973
+ function disjoint(a, b, sets) {
974
+ const ma = normMember(a);
975
+ const mb = normMember(b);
976
+ return ma !== "" && mb !== "" && ma !== mb && sets.some((s) => s.has(ma) && s.has(mb));
977
+ }
978
+ var relationKey = (p) => `${p.head}(${p.theme ?? ""})`;
979
+ function parseQuantified(clause, line, tools) {
980
+ let guard = TRUE;
981
+ let body = clause.trim();
982
+ const ifm = body.match(/^IF\b(.+?)\bTHEN\b(.+)$/i);
983
+ if (ifm) {
984
+ guard = parseCondition(ifm[1] ?? "");
985
+ body = (ifm[2] ?? "").trim();
986
+ }
987
+ const only = body.match(EXCLUSIVE_RE);
988
+ if (only) {
989
+ const p = parsePredicate(only[1] ?? "", tools);
990
+ if (!p || p.confidence !== "lexicon" || p.roles.goal === void 0) return null;
991
+ return { relation: relationKey(p), target: p.roles.goal, scopeText: (only[1] ?? "").replace(/[.,;:!?]+$/, "").trim(), guard, line };
992
+ }
993
+ const qm = body.match(SUBJECT_QUANT_RE);
994
+ const im = qm ? null : body.match(/^(this|that|the)\s+(\w+)\s+(.+)$/i);
995
+ const m = qm ?? im;
996
+ if (!m) return null;
997
+ const kind = qm ? KIND_OF.get((qm[1] ?? "").toLowerCase()) ?? "existential" : "existential";
998
+ const restrictor = normNoun(m[2] ?? "");
999
+ const { negated: surfaceNeg, text } = parseScope(m[3] ?? "");
1000
+ if (text === "") return null;
1001
+ const { key, parsed } = canonicalActionKey(text, tools);
1002
+ const polarityNeg = parsed?.confidence === "lexicon" && parsed.lexicalPolarity === -1;
1003
+ const negated = surfaceNeg !== polarityNeg;
1004
+ return { det: (m[1] ?? "").toLowerCase(), kind, restrictor, scope: key, scopeText: text, parsed, negated, corner: corner(kind, negated), guard, line };
1005
+ }
1006
+ var isExclusive = (x) => "relation" in x;
1007
+ function describe(c) {
1008
+ const q = c.det === "this" || c.det === "that" || c.det === "the" ? `this ${c.restrictor}` : `${c.det} ${c.restrictor}`;
1009
+ return `${q} is ${c.negated ? "not " : ""}"${c.scopeText}"`;
1010
+ }
1011
+ function parseCardinal(clause, line) {
1012
+ const g = splitGuard(clause.trim());
1013
+ const guard = g ? parseCondition(g.cond) : TRUE;
1014
+ const body = g ? g.body : clause.trim();
1015
+ const bound = parseCardinalBody(body);
1016
+ return bound === null ? null : { ...bound, guard, line };
1017
+ }
1018
+ function collectActionCounts(clause, line) {
1019
+ const out = [];
1020
+ for (const sentence of splitSentences(clause).map((x) => x.text.trim()).filter((x) => x !== "")) {
1021
+ if (/\bOTHERWISE\b/i.test(sentence)) continue;
1022
+ const g = splitGuard(sentence);
1023
+ const guard = g ? parseCondition(g.cond) : TRUE;
1024
+ const body = g ? g.body : sentence;
1025
+ for (const seg of body.split(/[;,]/)) {
1026
+ const t = seg.trim().replace(/^then\s+/i, "");
1027
+ if (t === "") continue;
1028
+ const lead = stripDeonticLead(t, { peelSubject: true });
1029
+ if (lead.prohibited) continue;
1030
+ const ac = parseActionCount(lead.body);
1031
+ if (ac) out.push({ ...ac, guard, line, subject: lead.subject });
1032
+ }
1033
+ }
1034
+ return out;
1035
+ }
1036
+ function parseAll(rawLines) {
1037
+ const exempt = proseExemptLines(rawLines);
1038
+ const tools = /* @__PURE__ */ new Set();
1039
+ const clauses = [];
1040
+ const exclusives = [];
1041
+ const cardinals = [];
1042
+ const actions = [];
1043
+ rawLines.forEach((raw, line) => {
1044
+ if (exempt.has(line)) return;
1045
+ const clause = maskQuotes(parseLine(raw).clause).trim();
1046
+ if (clause === "") return;
1047
+ actions.push(...collectActionCounts(clause, line));
1048
+ const card = parseCardinal(clause, line);
1049
+ if (card) {
1050
+ cardinals.push(card);
1051
+ return;
1052
+ }
1053
+ const parsed = parseQuantified(clause, line, tools);
1054
+ if (!parsed) return;
1055
+ if (isExclusive(parsed)) exclusives.push(parsed);
1056
+ else clauses.push(parsed);
1057
+ });
1058
+ return { clauses, exclusives, cardinals, actions };
1059
+ }
1060
+ function collectQuantified(rawLines) {
1061
+ return parseAll(rawLines).clauses;
1062
+ }
1063
+ function checkQuantifier(rawLines) {
1064
+ const parsed = parseAll(rawLines);
1065
+ const { clauses, exclusives } = parsed;
1066
+ const addr = addresseeSubjects(rawLines);
1067
+ const foldSubject = (sub) => sub !== "you" && addr.has(sub) ? "you" : sub;
1068
+ const actions = parsed.actions.map((a) => ({ ...a, subject: foldSubject(a.subject) }));
1069
+ const cardinals = parsed.cardinals.map((c) => c.owner === void 0 ? c : { ...c, owner: foldSubject(c.owner) });
1070
+ for (const [key, obj] of collectObjects(rawLines)) {
1071
+ const members = obj.listMembers ?? [];
1072
+ if (members.length === 0) continue;
1073
+ cardinals.push({
1074
+ noun: singularizeNoun(key),
1075
+ lo: members.length,
1076
+ hi: Infinity,
1077
+ text: `${key} INCLUDING ${members.join(", ")}`,
1078
+ tail: "",
1079
+ guard: TRUE,
1080
+ line: obj.lines[0] ?? 0,
1081
+ fromList: true
1082
+ });
1083
+ }
1084
+ const exempt = proseExemptLines(rawLines);
1085
+ const tools = /* @__PURE__ */ new Set();
1086
+ const disjointSets = [];
1087
+ for (const raw of rawLines) {
1088
+ const e = findEnumDeclaration(raw);
1089
+ if (e) disjointSets.push(new Set(e.members.map(normMember).filter((x) => x !== "")));
1090
+ }
1091
+ const out = [];
1092
+ const seen = /* @__PURE__ */ new Set();
1093
+ for (let i = 0; i < clauses.length; i += 1) {
1094
+ for (let j = i + 1; j < clauses.length; j += 1) {
1095
+ const a = clauses[i];
1096
+ const b = clauses[j];
1097
+ if (a.restrictor !== b.restrictor || a.corner === null || b.corner === null) continue;
1098
+ if (!guardsCollide(a.guard, b.guard)) continue;
1099
+ let rel = null;
1100
+ if (a.scope === b.scope) {
1101
+ rel = opposes(a.corner, b.corner);
1102
+ } else if (disjoint(a.scopeText, b.scopeText, disjointSets)) {
1103
+ const reframe = (c) => c === "A" ? "E" : c === "I" ? "O" : null;
1104
+ const rb = reframe(b.corner);
1105
+ const ra = reframe(a.corner);
1106
+ rel = rb && opposes(a.corner, rb) || ra && opposes(b.corner, ra) || null;
1107
+ }
1108
+ const demonstrative = (c) => c.det === "this" || c.det === "that" || c.det === "the";
1109
+ const subaltern = (x, y) => x === "A" && y === "I" || x === "E" && y === "O";
1110
+ if (rel === null && a.scope === b.scope && !demonstrative(a) && !demonstrative(b) && (subaltern(a.corner, b.corner) || subaltern(b.corner, a.corner))) {
1111
+ const strong = a.corner === "A" || a.corner === "E" ? a : b;
1112
+ const weak = strong === a ? b : a;
1113
+ const lines2 = [.../* @__PURE__ */ new Set([a.line, b.line])].sort((x, y) => x - y);
1114
+ const dedupe2 = `strength|${lines2.join(",")}`;
1115
+ if (seen.has(dedupe2)) continue;
1116
+ seen.add(dedupe2);
1117
+ const exception = strong.corner === "A" ? `Some ${weak.restrictor}s are not ${weak.scopeText}.` : `Some ${weak.restrictor}s are ${weak.scopeText}.`;
1118
+ out.push({
1119
+ lines: lines2,
1120
+ kind: "strength",
1121
+ message: `One quantity per fact \u2014 ${describe(strong)} already entails ${describe(weak)}, so this fact carries two strengths (a model may read the weaker "some" as "not all"). Keep the stronger line, or state the exception you meant: "${exception}".`
1122
+ });
1123
+ continue;
1124
+ }
1125
+ if (rel !== "hard" && !(rel === "contrary" && QUANT_CONTRARY_FIRES)) continue;
1126
+ const lines = [.../* @__PURE__ */ new Set([a.line, b.line])].sort((x, y) => x - y);
1127
+ const dedupe = lines.join(",");
1128
+ if (seen.has(dedupe)) continue;
1129
+ seen.add(dedupe);
1130
+ const majority = a.corner.startsWith("M") || b.corner.startsWith("M");
1131
+ const proof = rel === "contrary" ? cardinals.find((c) => c.owner === void 0 && c.tail === "" && c.lo >= 1 && (c.noun === a.restrictor || pluralizeNoun(c.noun) === a.restrictor || c.noun === singularizeNoun(a.restrictor))) : void 0;
1132
+ out.push({
1133
+ lines,
1134
+ kind: majority ? "majority" : "opposition",
1135
+ message: majority ? `Majority conflict \u2014 ${describe(a)} and ${describe(b)} can't both hold; each needs over half of "${a.restrictor}".` : `Quantifier conflict \u2014 ${describe(a)}, but ${describe(b)}. Both cannot hold${proof !== void 0 ? ` \u2014 and "${proof.text}" proves the set is nonempty` : ""}.`
1136
+ });
1137
+ }
1138
+ }
1139
+ for (let i = 0; i < cardinals.length; i += 1) {
1140
+ for (let j = i + 1; j < cardinals.length; j += 1) {
1141
+ const a = cardinals[i];
1142
+ const b = cardinals[j];
1143
+ if (a.noun !== b.noun || (a.owner ?? "") !== (b.owner ?? "") || a.tail !== b.tail) continue;
1144
+ if (!guardsCollide(a.guard, b.guard)) continue;
1145
+ const lines = [.../* @__PURE__ */ new Set([a.line, b.line])].sort((x, y) => x - y);
1146
+ const setName = `${pluralizeNoun(a.noun)}${a.tail !== "" ? ` ${a.tail}` : ""}`;
1147
+ if (Math.max(a.lo, b.lo) <= Math.min(a.hi, b.hi)) {
1148
+ const inside = (x, y) => y.lo <= x.lo && x.hi <= y.hi;
1149
+ const counted = (x) => x.fromList !== true && x.phrase !== "a" && x.phrase !== "an";
1150
+ if (!counted(a) || !counted(b)) continue;
1151
+ if (a.lo === b.lo && a.hi === b.hi) continue;
1152
+ if (!inside(a, b) && !inside(b, a)) continue;
1153
+ const strong = inside(a, b) ? a : b;
1154
+ const weak = strong === a ? b : a;
1155
+ const dedupe2 = `cardstrength|${lines.join(",")}`;
1156
+ if (seen.has(dedupe2)) continue;
1157
+ seen.add(dedupe2);
1158
+ out.push({
1159
+ lines,
1160
+ kind: "strength",
1161
+ message: `One count per set \u2014 "${strong.text}" (${boundText(strong.lo, strong.hi)}) already entails "${weak.text}" (${boundText(weak.lo, weak.hi)}): two strengths for "${setName}". Keep the stronger claim.`
1162
+ });
1163
+ continue;
1164
+ }
1165
+ const dedupe = `card|${lines.join(",")}`;
1166
+ if (seen.has(dedupe)) continue;
1167
+ seen.add(dedupe);
1168
+ const list = a.fromList === true ? a : b.fromList === true ? b : null;
1169
+ const stated = list === a ? b : a;
1170
+ out.push({
1171
+ lines,
1172
+ kind: "cardinality",
1173
+ message: list !== null ? `The list disproves the count \u2014 "${list.text}" names ${list.lo} member${list.lo === 1 ? "" : "s"}, but "${stated.text}" bounds "${setName}" to ${boundText(stated.lo, stated.hi)}.` : `Cardinality conflict \u2014 "${a.text}" (${boundText(a.lo, a.hi)}) but "${b.text}" (${boundText(b.lo, b.hi)}). The counts cannot both hold \u2014 state how many "${setName}" ${a.owner !== void 0 ? `"${a.owner}" has` : "there are"} once.`
1174
+ });
1175
+ }
1176
+ }
1177
+ const orderedStep = /* @__PURE__ */ new Set();
1178
+ rawLines.forEach((raw, i) => {
1179
+ if (/^\s*\d+(?:\.\d+)*[.)]\s/.test(raw)) orderedStep.add(i);
1180
+ });
1181
+ for (let i = 0; i < actions.length; i += 1) {
1182
+ for (let j = i + 1; j < actions.length; j += 1) {
1183
+ const a = actions[i];
1184
+ const b = actions[j];
1185
+ if (a.line === b.line) continue;
1186
+ if (orderedStep.has(a.line) && orderedStep.has(b.line)) continue;
1187
+ if (a.verb !== b.verb || a.noun !== b.noun || a.tail !== b.tail) continue;
1188
+ if (a.subject !== b.subject) continue;
1189
+ if (a.lo === b.lo && a.hi === b.hi) continue;
1190
+ if (!guardsCollide(a.guard, b.guard)) continue;
1191
+ const lines = [.../* @__PURE__ */ new Set([a.line, b.line])].sort((x, y) => x - y);
1192
+ const dedupe = `act|${lines.join(",")}|${a.verb}|${a.noun}`;
1193
+ if (seen.has(dedupe)) continue;
1194
+ seen.add(dedupe);
1195
+ const slot = `${a.verb} ${pluralizeNoun(a.noun)}${a.tail !== "" ? ` ${a.tail}` : ""}`;
1196
+ out.push({
1197
+ lines,
1198
+ kind: "action-count",
1199
+ message: `One action, two counts \u2014 "${a.text}" but "${b.text}". A directive carries ONE count for "${slot}" \u2014 state how many once.`
1200
+ });
1201
+ }
1202
+ }
1203
+ for (const supply of cardinals) {
1204
+ if (supply.owner !== void 0 || supply.hi === Infinity) continue;
1205
+ for (const demand of actions) {
1206
+ if (supply.noun !== demand.noun || supply.tail !== demand.tail) continue;
1207
+ if (demand.lo <= supply.hi) continue;
1208
+ if (!guardsCollide(supply.guard, demand.guard)) continue;
1209
+ const lines = [.../* @__PURE__ */ new Set([supply.line, demand.line])].sort((x, y) => x - y);
1210
+ const dedupe = `sup|${lines.join(",")}|${demand.verb}|${demand.noun}`;
1211
+ if (seen.has(dedupe)) continue;
1212
+ seen.add(dedupe);
1213
+ out.push({
1214
+ lines,
1215
+ kind: "insufficient-set",
1216
+ message: `The set cannot cover the directive \u2014 "${demand.text}" needs ${demand.lo} ${pluralizeNoun(demand.noun)}, but "${supply.text}" bounds the set to ${boundText(supply.lo, supply.hi)}. Raise the count or shrink the demand.`
1217
+ });
1218
+ }
1219
+ }
1220
+ for (const ex of exclusives) {
1221
+ if (!guardsCollide(ex.guard, TRUE)) continue;
1222
+ rawLines.forEach((raw, line) => {
1223
+ if (exempt.has(line) || line === ex.line) return;
1224
+ const clause = maskQuotes(parseLine(raw).clause).trim();
1225
+ const only = clause.match(EXCLUSIVE_RE);
1226
+ const p = parsePredicate(only ? only[1] ?? "" : clause, tools);
1227
+ if (!p || p.confidence !== "lexicon" || relationKey(p) !== ex.relation) return;
1228
+ const goal = p.roles.goal;
1229
+ if (goal === void 0 || goal === ex.target) return;
1230
+ const lines = [ex.line, line].sort((x, y) => x - y);
1231
+ const key = `only|${lines.join(",")}`;
1232
+ if (seen.has(key)) return;
1233
+ seen.add(key);
1234
+ out.push({ lines, kind: "exclusivity", message: `Exclusivity violation \u2014 "only ${ex.scopeText}" closes this to "${ex.target}", but "${goal}" is added. Drop one, or remove "only".` });
1235
+ });
1236
+ }
1237
+ return out;
1238
+ }
1239
+ function checkSyllogism(rawLines) {
1240
+ const clauses = collectQuantified(rawLines).filter((c) => (c.corner === "A" || c.corner === "E") && c.guard.t === "true");
1241
+ const setKey = (s) => s.toLowerCase().replace(/^(?:a|an|the)\s+/, "").replace(/s$/, "").trim();
1242
+ const subsetEdges = [];
1243
+ const disjointEdges = [];
1244
+ for (const c of clauses) {
1245
+ const s = setKey(c.restrictor);
1246
+ const p = setKey(c.scope);
1247
+ if (s === p || s === "" || p === "") continue;
1248
+ if (c.corner === "A") subsetEdges.push({ s, p, line: c.line });
1249
+ else disjointEdges.push({ x: s, y: p, line: c.line });
1250
+ }
1251
+ if (subsetEdges.length === 0 || disjointEdges.length === 0) return [];
1252
+ const adj = /* @__PURE__ */ new Map();
1253
+ for (const e of subsetEdges) adj.set(e.s, [...adj.get(e.s) ?? [], { to: e.p, line: e.line }]);
1254
+ const pathsFrom = (src) => {
1255
+ const seen = /* @__PURE__ */ new Map([[src, []]]);
1256
+ const q = [src];
1257
+ while (q.length > 0) {
1258
+ const n = q.shift();
1259
+ for (const e of adj.get(n) ?? []) {
1260
+ if (!seen.has(e.to)) {
1261
+ seen.set(e.to, [...seen.get(n) ?? [], e.line]);
1262
+ q.push(e.to);
1263
+ }
1264
+ }
1265
+ }
1266
+ return seen;
1267
+ };
1268
+ const witnesses = /* @__PURE__ */ new Set([...subsetEdges.map((e) => e.s), ...disjointEdges.flatMap((d) => [d.x, d.y])]);
1269
+ const out = [];
1270
+ const emitted = /* @__PURE__ */ new Set();
1271
+ for (const d of disjointEdges) {
1272
+ for (const w of witnesses) {
1273
+ const reach = pathsFrom(w);
1274
+ const px = reach.get(d.x);
1275
+ const py = reach.get(d.y);
1276
+ if (!px || !py) continue;
1277
+ const lines = [.../* @__PURE__ */ new Set([...px, ...py, d.line])].sort((a, b) => a - b);
1278
+ if (lines.length < 3) continue;
1279
+ const key = lines.join(",");
1280
+ if (emitted.has(key)) continue;
1281
+ emitted.add(key);
1282
+ out.push({ lines, message: `Contradictory chain \u2014 the rules on these lines force an overlap between "${d.x}" and "${d.y}", which they also declare mutually exclusive. Relax one rule.` });
1283
+ }
1284
+ }
1285
+ return out;
1286
+ }
1287
+
1288
+ // src/core/security.ts
1289
+ var SECRET_PATTERNS = [
1290
+ { re: /\bsk-ant-[A-Za-z0-9_-]{16,}/g, what: "Anthropic API key", severity: "error" },
1291
+ { re: /\bsk-(?!ant-)[A-Za-z0-9]{20,}/g, what: "OpenAI-style API key", severity: "error" },
1292
+ { re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g, what: "GitHub token", severity: "error" },
1293
+ { re: /\bgithub_pat_[A-Za-z0-9_]{20,}/g, what: "GitHub fine-grained token", severity: "error" },
1294
+ { re: /\bAKIA[0-9A-Z]{16}\b/g, what: "AWS access key id", severity: "error" },
1295
+ { re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/g, what: "Slack token", severity: "error" },
1296
+ { re: /\bAIza[0-9A-Za-z_-]{30,}/g, what: "Google API key", severity: "error" },
1297
+ { re: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g, what: "Stripe key", severity: "error" },
1298
+ { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g, what: "private key material", severity: "error" },
1299
+ { re: /\bBearer\s+[A-Za-z0-9_\-.=+/]{25,}/g, what: "bearer token", severity: "error" },
1300
+ // Generic assignment (`api_key: …`, `password=…`) — warn: precise enough to surface, but a
1301
+ // documented config shape can look identical, so it never gates on its own.
1302
+ {
1303
+ re: /\b(?:api[_-]?key|apikey|secret|passwd|password|auth[_-]?token|access[_-]?token)\s*[:=]\s*['"]?([A-Za-z0-9_\-.+/]{12,})/gi,
1304
+ what: "credential assignment",
1305
+ severity: "warn"
1306
+ }
1307
+ ];
1308
+ var PLACEHOLDER = /\{|\$|<|\bYOUR[_-]|\bEXAMPLE\b|X{4,}|\*{3,}|\.\.\./i;
1309
+ function leakedSecrets(line, i) {
1310
+ const out = [];
1311
+ for (const p of SECRET_PATTERNS) {
1312
+ p.re.lastIndex = 0;
1313
+ let m;
1314
+ while ((m = p.re.exec(line)) !== null) {
1315
+ const value = m[1] ?? m[0];
1316
+ if (PLACEHOLDER.test(value)) continue;
1317
+ out.push({
1318
+ source: "security/leaked-secret",
1319
+ severity: p.severity,
1320
+ line: i,
1321
+ start: m.index,
1322
+ end: m.index + m[0].length,
1323
+ message: `Leaked secret \u2014 this looks like a real ${p.what}. Remove it and rotate the credential; reference it as a {variable} instead.`
1324
+ });
1325
+ }
1326
+ }
1327
+ return out;
1328
+ }
1329
+ var INJECTION_PATTERNS = [
1330
+ {
1331
+ re: /\b(?:ignore|disregard|forget)\s+(?:all\s+|any\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|rules?|messages?)/gi,
1332
+ what: "instruction-override phrasing"
1333
+ },
1334
+ { re: /\byou\s+are\s+now\s+(?:DAN|jailbroken|unrestricted)\b/gi, what: "role-hijack phrasing" },
1335
+ { re: /\b(?:pretend|act)\s+(?:that\s+)?you\s+have\s+no\s+(?:restrictions|guidelines|rules)\b/gi, what: "restriction-bypass phrasing" },
1336
+ { re: /\bdo\s+anything\s+now\b/gi, what: "DAN-style phrasing" }
1337
+ ];
1338
+ function injectionPatterns(line, i) {
1339
+ const masked = maskQuotes(line);
1340
+ const out = [];
1341
+ for (const p of INJECTION_PATTERNS) {
1342
+ p.re.lastIndex = 0;
1343
+ let m;
1344
+ while ((m = p.re.exec(masked)) !== null) {
1345
+ out.push({
1346
+ source: "security/injection",
1347
+ severity: "warn",
1348
+ line: i,
1349
+ start: m.index,
1350
+ end: m.index + m[0].length,
1351
+ message: `Injection pattern \u2014 "${m[0]}" is ${p.what}. If this is a rule ABOUT such phrases, quote the phrase to mark it literal.`
1352
+ });
1353
+ }
1354
+ }
1355
+ return out;
1356
+ }
1357
+
1358
+ // src/core/coverage.ts
1359
+ var STOP = /* @__PURE__ */ new Set([
1360
+ "the",
1361
+ "a",
1362
+ "an",
1363
+ "to",
1364
+ "of",
1365
+ "for",
1366
+ "and",
1367
+ "or",
1368
+ "not",
1369
+ "is",
1370
+ "are",
1371
+ "be",
1372
+ "you",
1373
+ "your",
1374
+ "it",
1375
+ "that",
1376
+ "this",
1377
+ "with",
1378
+ "in",
1379
+ "on",
1380
+ "at",
1381
+ "by",
1382
+ "do",
1383
+ "does",
1384
+ "must",
1385
+ "never",
1386
+ "always",
1387
+ "should",
1388
+ "may",
1389
+ "every",
1390
+ "any",
1391
+ "all",
1392
+ "when",
1393
+ "if",
1394
+ "then",
1395
+ "than",
1396
+ "as",
1397
+ "so",
1398
+ "no",
1399
+ "yes"
1400
+ ]);
1401
+ function tokens(text) {
1402
+ const out = /* @__PURE__ */ new Set();
1403
+ for (const w of text.toLowerCase().match(/[a-z][a-z'-]{2,}/g) ?? []) {
1404
+ const t = w.replace(/'s$/, "").replace(/s$/, "");
1405
+ if (!STOP.has(w) && !STOP.has(t) && t.length >= 3) out.add(t);
1406
+ }
1407
+ return out;
1408
+ }
1409
+ function caseTokens(suite, id) {
1410
+ const parts = [];
1411
+ const tc = suite.testCases.find((c) => c.id === id);
1412
+ if (tc) parts.push(tc.input, tc.expectation ?? "");
1413
+ for (const a of suite.asserts[id] ?? []) {
1414
+ if (a.kind === "contains") parts.push(a.value);
1415
+ else if (a.kind === "matches") parts.push(a.source.replace(/\\[a-z]/gi, " "));
1416
+ else if (a.kind === "atMost" || a.kind === "atLeast") parts.push(a.unit);
1417
+ }
1418
+ return tokens(parts.join(" "));
1419
+ }
1420
+ function sameWord(a, b) {
1421
+ if (a === b) return true;
1422
+ return a.length >= 4 && b.length >= 4 && (a.startsWith(b) || b.startsWith(a));
1423
+ }
1424
+ function hasWord(toks, word) {
1425
+ for (const t of toks) if (sameWord(t, word)) return true;
1426
+ return false;
1427
+ }
1428
+ function linked(rule, caseToks) {
1429
+ const head = rule.predicate?.head;
1430
+ const theme = rule.predicate?.theme;
1431
+ const themeToks = theme ? tokens(theme) : /* @__PURE__ */ new Set();
1432
+ if (head && hasWord(caseToks, head)) return true;
1433
+ for (const t of themeToks) if (hasWord(caseToks, t)) return true;
1434
+ const ruleToks = tokens(rule.action.text);
1435
+ let shared = 0;
1436
+ for (const t of ruleToks) if (hasWord(caseToks, t)) shared += 1;
1437
+ return shared >= 2;
1438
+ }
1439
+ function ruleCoverage(ir, suite) {
1440
+ const hard = ir.nodes.filter(
1441
+ (n) => n.kind === "directive" && (n.deontic === "obligate" || n.deontic === "prohibit")
1442
+ );
1443
+ const tokensByCase = new Map(suite.testCases.map((c) => [c.id, caseTokens(suite, c.id)]));
1444
+ const perRule = hard.map((rule) => {
1445
+ const cases = [...tokensByCase.entries()].filter(([, toks]) => linked(rule, toks)).map(([id]) => id);
1446
+ return { line: rule.line, action: rule.action.text, covered: cases.length > 0, cases };
1447
+ });
1448
+ const coveredRate = hard.length === 0 ? 1 : perRule.filter((r) => r.covered).length / hard.length;
1449
+ return { perRule, coveredRate };
1450
+ }
1451
+
1452
+ // src/core/diagnostics.ts
1453
+ var SEV_RANK = { info: 1, warn: 2, error: 3 };
1454
+ var WORDY = [
1455
+ { re: /\bin order to\b/gi, to: "to" },
1456
+ { re: /\bdue to the fact that\b/gi, to: "because" },
1457
+ { re: /\bat this point in time\b/gi, to: "now" },
1458
+ { re: /\bin the event that\b/gi, to: "if" },
1459
+ { re: /\bfor the purpose of\b/gi, to: "to" },
1460
+ { re: /\bwith regard to\b/gi, to: "about" },
1461
+ { re: /\ba large number of\b/gi, to: "many" },
1462
+ { re: /\bin spite of the fact that\b/gi, to: "although" }
1463
+ ];
1464
+ function wordy(line, i) {
1465
+ const out = [];
1466
+ for (const w of WORDY) {
1467
+ w.re.lastIndex = 0;
1468
+ let m;
1469
+ while ((m = w.re.exec(line)) !== null) {
1470
+ out.push({
1471
+ source: "style/wordy",
1472
+ severity: "info",
1473
+ line: i,
1474
+ start: m.index,
1475
+ end: m.index + m[0].length,
1476
+ message: `Wordy \u2014 "${m[0]}" can be "${w.to}".`,
1477
+ fix: { title: `\u2192 "${w.to}"`, replacement: w.to }
1478
+ });
1479
+ }
1480
+ }
1481
+ return out;
1482
+ }
1483
+ var WHOLE_LINE_QUOTE = /^\s*"([^"]*)"\s*$/;
1484
+ function quotedProse(line, i) {
1485
+ const m = line.match(WHOLE_LINE_QUOTE);
1486
+ if (m === null) return [];
1487
+ const start = line.indexOf('"');
1488
+ return [
1489
+ {
1490
+ source: "style/quoted-prose",
1491
+ severity: "warn",
1492
+ line: i,
1493
+ start,
1494
+ end: line.lastIndexOf('"') + 1,
1495
+ message: 'A whole-line quote hides this statement from the checker \u2014 reserve "\u2026" for exact words to say (an utterance after a speech verb, or a sample in <examples>). Unquote it: a plain statement renders and stays checkable.',
1496
+ fix: { title: "Unquote", replacement: m[1] ?? "" }
1497
+ }
1498
+ ];
1499
+ }
1500
+ var TERMINATED = /[.!?:]["')\]]*\s*$/;
1501
+ function unterminated(line, i, end) {
1502
+ const start = Math.max(0, Math.min(line.length - line.trimStart().length, end - 1));
1503
+ return {
1504
+ source: "structure/missing-period",
1505
+ severity: "error",
1506
+ line: i,
1507
+ start,
1508
+ end,
1509
+ message: 'Unterminated statement \u2014 end it with a period (or ! ?; a lead-in may end with ":"). Statement boundaries are a compile contract.',
1510
+ fix: { title: 'Add "."', replacement: `${line.slice(start, end)}.` }
1511
+ };
1512
+ }
1513
+ function missingPeriod(line, i) {
1514
+ const stmt = stripInlineComment(line);
1515
+ const end = stmt.trimEnd().length;
1516
+ if (end === 0 || TERMINATED.test(stmt)) return [];
1517
+ if (TRANSCLUSION_LINE_RE.test(stmt)) return [];
1518
+ return [unterminated(line, i, end)];
1519
+ }
1520
+ function missingPeriodLiteral(line, i) {
1521
+ const end = line.trimEnd().length;
1522
+ if (end === 0 || TERMINATED.test(line)) return [];
1523
+ return [unterminated(line, i, end)];
1524
+ }
1525
+ function oneOfColon(line, i) {
1526
+ const masked = maskQuotes(stripInlineComment(line));
1527
+ const m = /\bone\s+of\s*:(?=\s*\S)/i.exec(masked);
1528
+ if (!m) return [];
1529
+ const start = m.index;
1530
+ const end = m.index + m[0].length;
1531
+ return [
1532
+ {
1533
+ source: "structure/one-of-colon",
1534
+ severity: "error",
1535
+ line: i,
1536
+ start,
1537
+ end,
1538
+ message: `"one of" never takes a colon \u2014 the quantifier is the same everywhere (declarations, domains, tool types, conditions). Write "${line.slice(start, end - 1).trimEnd()} ${line.slice(end).trimStart().split(",")[0] ?? ""}, \u2026" \u2014 just drop the ":".`,
1539
+ fix: { title: 'Drop ":"', replacement: line.slice(start, end - 1).trimEnd() }
1540
+ }
1541
+ ];
1542
+ }
1543
+ function inlineMarker(line, i) {
1544
+ const text = maskQuotes(stripAnnotation(stripInlineComment(line)));
1545
+ const head = line.match(DEF_ENTRY);
1546
+ const scanStart = head ? (head[1] ?? "").length + (head[2] ?? "").length + (head[3] ?? "").length + 2 : 0;
1547
+ const out = [];
1548
+ for (const m of text.matchAll(/:{2,}/g)) {
1549
+ const ci = m.index ?? 0;
1550
+ if (ci < scanStart) continue;
1551
+ let s = ci;
1552
+ let e = ci + m[0].length;
1553
+ while (s > 0 && /[A-Za-z0-9_?-]/.test(line[s - 1] ?? "")) s -= 1;
1554
+ while (e < line.length && /[A-Za-z0-9_?-]/.test(line[e] ?? "")) e += 1;
1555
+ const sample = line.slice(s, e);
1556
+ out.push({
1557
+ source: "structure/inline-marker",
1558
+ severity: "error",
1559
+ line: i,
1560
+ start: s,
1561
+ end: e,
1562
+ message: `"::" opens a definition at a line head \u2014 here it is inline. Give the second object its own "- " bullet (a sub-definition), or quote "${sample}" as a sample.`,
1563
+ fix: { title: `Quote "${sample}"`, replacement: `"${sample}"` }
1564
+ });
1565
+ }
1566
+ return out;
1567
+ }
1568
+ function specialCharacters(line, i) {
1569
+ const text = maskQuotes(stripAnnotation(stripInlineComment(line)));
1570
+ if (!SPECIAL_ANY.test(text)) return [];
1571
+ const out = [];
1572
+ const claimed = new Array(text.length).fill(false);
1573
+ for (const p of CHARSET_RULES) {
1574
+ for (const m of text.matchAll(p.re)) {
1575
+ const s = m.index ?? 0;
1576
+ const e = s + m[0].length;
1577
+ if (e === s) continue;
1578
+ let clash = false;
1579
+ for (let k = s; k < e; k += 1) if (claimed[k]) clash = true;
1580
+ if (clash) continue;
1581
+ for (let k = s; k < e; k += 1) claimed[k] = true;
1582
+ const fix = p.fix ? p.fix(m, text) : null;
1583
+ out.push({
1584
+ source: p.source,
1585
+ severity: "error",
1586
+ line: i,
1587
+ start: s,
1588
+ end: e,
1589
+ message: p.msg,
1590
+ ...fix ? { fix } : {}
1591
+ });
1592
+ }
1593
+ }
1594
+ return out;
1595
+ }
1596
+ var TRAILING_WS = /[ \t]+$/;
1597
+ function trailingSpace(line, i) {
1598
+ const m = line.match(TRAILING_WS);
1599
+ if (!m || m.index === void 0 || line.trim() === "") return [];
1600
+ return [
1601
+ {
1602
+ source: "structure/trailing-space",
1603
+ severity: "error",
1604
+ line: i,
1605
+ start: m.index,
1606
+ end: line.length,
1607
+ message: "Trailing whitespace: invisible noise every diff and every token count carries. Strip it.",
1608
+ fix: { title: "Strip", replacement: "" }
1609
+ }
1610
+ ];
1611
+ }
1612
+ function tabCharacters(line, i) {
1613
+ const out = [];
1614
+ for (let k = line.indexOf(" "); k !== -1; k = line.indexOf(" ", k + 1)) {
1615
+ out.push({
1616
+ source: "structure/tab",
1617
+ severity: "error",
1618
+ line: i,
1619
+ start: k,
1620
+ end: k + 1,
1621
+ message: "Tab character: the indent unit is 2 spaces \u2014 a tab is an invisible, variable-width impostor.",
1622
+ fix: { title: "\u2192 spaces", replacement: " " }
1623
+ });
1624
+ }
1625
+ return out;
1626
+ }
1627
+ var PHANTOM_RUN = /(?<=\S) {2,}(?=\S)/g;
1628
+ function phantomColumns(line, i, literalBody) {
1629
+ const masked = maskQuotes(line);
1630
+ const cut = literalBody ? masked.length : stripAnnotation(stripInlineComment(line)).trimEnd().length;
1631
+ const out = [];
1632
+ for (const m of masked.matchAll(PHANTOM_RUN)) {
1633
+ const s = m.index ?? 0;
1634
+ if (s > cut) continue;
1635
+ out.push({
1636
+ source: "structure/phantom-column",
1637
+ severity: "error",
1638
+ line: i,
1639
+ start: s,
1640
+ end: s + m[0].length,
1641
+ message: `Phantom column: a space run doing alignment's job. One space between things \u2014 commentary belongs in a // comment, layout tables in a fence or <"name"> zone.`,
1642
+ fix: { title: "\u2192 one space", replacement: " " }
1643
+ });
1644
+ }
1645
+ return out;
1646
+ }
1647
+ var HEDGES = /\b(try to|tries to|try your best to|do your best to|should probably|might want to|you may want to|it would be good to|it might be good to|if possible|when possible|where possible|as much as possible|attempt to|attempts to|strive to|aim to|feel free to|ideally|maybe|perhaps|kind of|sort of)\b/gi;
1648
+ function hedging(line, i) {
1649
+ const out = [];
1650
+ HEDGES.lastIndex = 0;
1651
+ let m;
1652
+ while ((m = HEDGES.exec(line)) !== null) {
1653
+ out.push({
1654
+ source: "prompt/hedging",
1655
+ severity: "info",
1656
+ line: i,
1657
+ start: m.index,
1658
+ end: m.index + m[0].length,
1659
+ message: `Hedging \u2014 "${m[0]}" turns this instruction into a suggestion the model may skip. Delete the hedge, or commit to a modal (${MODAL_HINT}).`
1660
+ });
1661
+ }
1662
+ return out;
1663
+ }
1664
+ var VAGUE = /\b(appropriately|appropriate|as needed|as appropriate|as necessary|as required|if needed|if necessary|if applicable|when necessary|when appropriate|where appropriate|in a timely manner|properly|reasonable|relevant|somehow|various|etc\.?|and so on|and so forth)\b/gi;
1665
+ function vague(line, i) {
1666
+ const out = [];
1667
+ VAGUE.lastIndex = 0;
1668
+ let m;
1669
+ while ((m = VAGUE.exec(line)) !== null) {
1670
+ out.push({
1671
+ source: "prompt/vague",
1672
+ severity: "info",
1673
+ line: i,
1674
+ start: m.index,
1675
+ end: m.index + m[0].length,
1676
+ message: `Vague \u2014 "${m[0]}" names a judgment call without the criterion to judge by, so nothing can check it. Replace it with a measurable form ("at most 3 sentences", "within 24 hours", "cite the source").`
1677
+ });
1678
+ }
1679
+ return out;
1680
+ }
1681
+ function wrappedFragments(rawLines, disp = (i) => i + 1) {
1682
+ const out = [];
1683
+ for (const [i, before] of wrappedContinuations(rawLines)) {
1684
+ const raw = rawLines[i] ?? "";
1685
+ const lead = raw.length - raw.trimStart().length;
1686
+ const body = raw.slice(lead);
1687
+ const capitalized = body.replace(/[a-z]/, (c) => c.toUpperCase());
1688
+ out.push({
1689
+ source: "structure/wrapped-fragment",
1690
+ severity: "warn",
1691
+ line: i,
1692
+ start: lead,
1693
+ end: raw.length,
1694
+ message: `Reads as a hard-wrapped continuation of line ${disp(before)} \u2014 it starts lowercase, classifies as plain prose, and line ${disp(before)} has no terminator. If they are ONE statement, join them (tg fmt --join); if this line stands alone, capitalize its first word (and terminate line ${disp(before)}).`,
1695
+ ...capitalized !== body ? { fix: { title: "Capitalize", replacement: capitalized } } : {}
1696
+ });
1697
+ }
1698
+ return out;
1699
+ }
1700
+ var QUALITY_ACTION = /^(?:be|being|stay|remain|sound|seem|appear|act|feel)\b/i;
1701
+ function unmeasurableDirectives(rawLines, exempt) {
1702
+ const out = [];
1703
+ const flagged = /* @__PURE__ */ new Set();
1704
+ const doc = buildStructure(rawLines);
1705
+ for (const node of lower(rawLines.join("\n"), { tools: doc.index.tools }).nodes) {
1706
+ if (node.kind !== "directive") continue;
1707
+ if (node.deontic !== "obligate" && node.deontic !== "prohibit") continue;
1708
+ if (node.predicate !== null || node.numeric !== null) continue;
1709
+ if (!QUALITY_ACTION.test(node.action.text.trim())) continue;
1710
+ if (exempt.has(node.line) || flagged.has(node.line)) continue;
1711
+ flagged.add(node.line);
1712
+ out.push({
1713
+ source: "prompt/unmeasurable",
1714
+ severity: "warn",
1715
+ line: node.line,
1716
+ start: node.span.start,
1717
+ end: node.span.end,
1718
+ message: 'Unmeasurable rule \u2014 no observable action or bound, so nothing can check compliance. Name a concrete action ("cite the source", "at most 3 sentences") or move it to a prose block.'
1719
+ });
1720
+ }
1721
+ const render = renderLines(rawLines);
1722
+ rawLines.forEach((raw, i) => {
1723
+ if (flagged.has(i) || exempt.has(i)) return;
1724
+ const r = render[i];
1725
+ if (!r || r.kind !== "content" || r.lineClass === "md-code" || r.lineClass === "md-heading") return;
1726
+ const clause = parseLine(raw).clause.trim();
1727
+ if (!/^be\s+\w/i.test(clause) || classifyClause(clause) !== "prose") return;
1728
+ out.push({
1729
+ source: "prompt/unmeasurable",
1730
+ severity: "warn",
1731
+ line: i,
1732
+ start: raw.length - raw.trimStart().length,
1733
+ end: raw.length,
1734
+ message: 'Unmeasurable rule \u2014 "be <quality>" names no observable action, so nothing can check compliance. Name a concrete action, or move it to a prose block (persona prose is allowed to be soft).'
1735
+ });
1736
+ });
1737
+ return out;
1738
+ }
1739
+ function untestedRules(rawLines, testLines) {
1740
+ if (testLines.size === 0) return [];
1741
+ const source = rawLines.join("\n");
1742
+ const suite = extractSuite(source);
1743
+ if (suite.testCases.length === 0) return [];
1744
+ const doc = buildStructure(rawLines);
1745
+ const coverage = ruleCoverage(lower(source, { tools: doc.index.tools }), suite);
1746
+ return coverage.perRule.filter((r) => !r.covered).map((r) => {
1747
+ const raw = rawLines[r.line] ?? "";
1748
+ return {
1749
+ source: "prompt/untested-rule",
1750
+ severity: "info",
1751
+ line: r.line,
1752
+ start: raw.length - raw.trimStart().length,
1753
+ end: raw.length,
1754
+ message: 'No $TEST case exercises this rule \u2014 its behavioral impact is unmeasurable until one does. Add a $TEST whose "- input::" triggers the rule, with a "- expect::" sub-bullet that checks the required behavior.'
1755
+ };
1756
+ });
1757
+ }
1758
+ var NUM_WORDS = {
1759
+ one: 1,
1760
+ two: 2,
1761
+ three: 3,
1762
+ four: 4,
1763
+ five: 5,
1764
+ six: 6,
1765
+ seven: 7,
1766
+ eight: 8,
1767
+ nine: 9,
1768
+ ten: 10
1769
+ };
1770
+ function determinableForm(text) {
1771
+ const t = text.trim();
1772
+ const q = /^(?:the\s+(?:reply|response|answer|output)\s+)?(?:mentions?|includes?|contains?|says?|quotes?|uses?|references?)\s+(?:the\s+(?:word|phrase|term)\s+)?"([^"]+)"[.!]?$/i.exec(t);
1773
+ if (q) return `contains "${q[1] ?? ""}"`;
1774
+ const w = /^(?:the\s+(?:reply|response|answer|output)\s+)?(?:mentions?|includes?|contains?|says?|references?|names?)\s+(?:the\s+(?:word|phrase|term)\s+)?([A-Za-z][A-Za-z0-9_-]*)[.!]?$/i.exec(t);
1775
+ if (w) return `contains "${w[1] ?? ""}"`;
1776
+ const b = /^(?:is\s+|stays?\s+(?:to\s+|within\s+)?|keeps?\s+(?:to\s+)?|responds?\s+in\s+)?(at\s+most|at\s+least|no\s+more\s+than|no\s+longer\s+than)\s+(\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+(sentences?|words?|characters?)(?:\s+(?:long|or\s+fewer|or\s+less|max))?[.!]?$/i.exec(t);
1777
+ if (b) {
1778
+ const rawN = (b[2] ?? "").toLowerCase();
1779
+ const n = /^\d+$/.test(rawN) ? Number(rawN) : NUM_WORDS[rawN];
1780
+ if (n === void 0) return null;
1781
+ const dir = /least/i.test(b[1] ?? "") ? "at least" : "at most";
1782
+ const unit = (b[3] ?? "").toLowerCase().replace(/s?$/, "s");
1783
+ return `${dir} ${n} ${unit}`;
1784
+ }
1785
+ return null;
1786
+ }
1787
+ function determinableExpects(rawLines) {
1788
+ return rubricExpectLines(rawLines).flatMap(({ line, start, text }) => {
1789
+ const form = determinableForm(text);
1790
+ if (form === null) return [];
1791
+ const raw = rawLines[line] ?? "";
1792
+ return [
1793
+ {
1794
+ source: "prompt/determinable-expect",
1795
+ severity: "info",
1796
+ line,
1797
+ start,
1798
+ end: raw.length,
1799
+ message: `Judge-scored expectation with a deterministic form \u2014 the sub-bullet \`- ${form}\` checks this free, instantly, with zero variance.`,
1800
+ fix: { title: `\u2192 ${form}`, replacement: form }
1801
+ }
1802
+ ];
1803
+ });
1804
+ }
1805
+ var DOUBLE_WORD = /\b(\w+)\s+\1\b/gi;
1806
+ function doubleWord(line, i) {
1807
+ const out = [];
1808
+ const masked = maskQuotes(line);
1809
+ DOUBLE_WORD.lastIndex = 0;
1810
+ let m;
1811
+ while ((m = DOUBLE_WORD.exec(masked)) !== null) {
1812
+ if (line[m.index - 1] === "#") continue;
1813
+ out.push({
1814
+ source: "grammar/double-word",
1815
+ severity: "warn",
1816
+ line: i,
1817
+ start: m.index,
1818
+ end: m.index + m[0].length,
1819
+ message: `Repeated word \u2014 "${m[0]}".`,
1820
+ fix: { title: "Remove duplicate", replacement: m[1] ?? m[0] }
1821
+ });
1822
+ }
1823
+ return out;
1824
+ }
1825
+ var MISSPELLINGS = {
1826
+ // ie / ei
1827
+ recieve: "receive",
1828
+ recieved: "received",
1829
+ reciept: "receipt",
1830
+ beleive: "believe",
1831
+ beleived: "believed",
1832
+ acheive: "achieve",
1833
+ acheived: "achieved",
1834
+ wierd: "weird",
1835
+ freind: "friend",
1836
+ peice: "piece",
1837
+ thier: "their",
1838
+ feild: "field",
1839
+ // doubled / undoubled consonants
1840
+ occured: "occurred",
1841
+ occuring: "occurring",
1842
+ occurance: "occurrence",
1843
+ occurence: "occurrence",
1844
+ occassion: "occasion",
1845
+ occassionally: "occasionally",
1846
+ refered: "referred",
1847
+ prefered: "preferred",
1848
+ begining: "beginning",
1849
+ commited: "committed",
1850
+ comming: "coming",
1851
+ runing: "running",
1852
+ stoped: "stopped",
1853
+ embarass: "embarrass",
1854
+ embarassing: "embarrassing",
1855
+ harrass: "harass",
1856
+ accomodate: "accommodate",
1857
+ paralell: "parallel",
1858
+ threshhold: "threshold",
1859
+ // -ant / -ent, -ance / -ence
1860
+ independant: "independent",
1861
+ persistant: "persistent",
1862
+ consistant: "consistent",
1863
+ existance: "existence",
1864
+ relevent: "relevant",
1865
+ // multi-word run together (non-words)
1866
+ alot: "a lot",
1867
+ infront: "in front",
1868
+ incase: "in case",
1869
+ aswell: "as well",
1870
+ // -ly / -ally / -ful
1871
+ definately: "definitely",
1872
+ seperately: "separately",
1873
+ basicly: "basically",
1874
+ publically: "publicly",
1875
+ succesfully: "successfully",
1876
+ immediatly: "immediately",
1877
+ completly: "completely",
1878
+ finaly: "finally",
1879
+ truely: "truly",
1880
+ usefull: "useful",
1881
+ greatful: "grateful",
1882
+ // high-frequency singles
1883
+ seperate: "separate",
1884
+ neccessary: "necessary",
1885
+ necesary: "necessary",
1886
+ accross: "across",
1887
+ untill: "until",
1888
+ wich: "which",
1889
+ teh: "the",
1890
+ hte: "the",
1891
+ becuase: "because",
1892
+ adress: "address",
1893
+ arguement: "argument",
1894
+ buisness: "business",
1895
+ concious: "conscious",
1896
+ dissapoint: "disappoint",
1897
+ enviroment: "environment",
1898
+ enviromental: "environmental",
1899
+ goverment: "government",
1900
+ garantee: "guarantee",
1901
+ gaurantee: "guarantee",
1902
+ guage: "gauge",
1903
+ langauge: "language",
1904
+ lenght: "length",
1905
+ strenght: "strength",
1906
+ tommorow: "tomorrow",
1907
+ tomorow: "tomorrow",
1908
+ writen: "written",
1909
+ succesful: "successful",
1910
+ sucessful: "successful",
1911
+ responce: "response",
1912
+ reccomend: "recommend",
1913
+ recomend: "recommend",
1914
+ priviledge: "privilege",
1915
+ familar: "familiar",
1916
+ foward: "forward"
1917
+ };
1918
+ var MISSPELL_RE = new RegExp(
1919
+ `\\b(${Object.keys(MISSPELLINGS).sort((a, b) => b.length - a.length).join("|")})\\b`,
1920
+ "gi"
1921
+ );
1922
+ function recase(correct, matched) {
1923
+ if (matched === matched.toUpperCase() && matched !== matched.toLowerCase()) return correct.toUpperCase();
1924
+ const first = matched[0] ?? "";
1925
+ if (first !== "" && first === first.toUpperCase() && first !== first.toLowerCase()) {
1926
+ return (correct[0] ?? "").toUpperCase() + correct.slice(1);
1927
+ }
1928
+ return correct;
1929
+ }
1930
+ function maskNonProse(line) {
1931
+ const blankSpan = (m) => " ".repeat(m.length);
1932
+ return line.replace(/\b(?:https?:\/\/|www\.)\S+/gi, blankSpan).replace(/`[^`]*`/g, blankSpan).replace(/[{[][^}\]]*[}\]]/g, blankSpan);
1933
+ }
1934
+ function spelling(line, i) {
1935
+ const out = [];
1936
+ const masked = maskNonProse(line);
1937
+ MISSPELL_RE.lastIndex = 0;
1938
+ let m;
1939
+ while ((m = MISSPELL_RE.exec(masked)) !== null) {
1940
+ const hit = m[0];
1941
+ const correct = MISSPELLINGS[hit.toLowerCase()];
1942
+ if (correct === void 0) continue;
1943
+ const fixed = recase(correct, hit);
1944
+ out.push({
1945
+ source: "spelling/typo",
1946
+ severity: "warn",
1947
+ line: i,
1948
+ start: m.index,
1949
+ end: m.index + hit.length,
1950
+ message: `Misspelling \u2014 "${hit}" should be "${fixed}".`,
1951
+ fix: { title: `\u2192 "${fixed}"`, replacement: fixed }
1952
+ });
1953
+ }
1954
+ return out;
1955
+ }
1956
+ var SPACE_BEFORE_PUNCT = /(\w|["')\]])(\s+)([,;:!?]+)/g;
1957
+ function spacing(line, i) {
1958
+ const out = [];
1959
+ const masked = maskNonProse(line);
1960
+ SPACE_BEFORE_PUNCT.lastIndex = 0;
1961
+ let m;
1962
+ while ((m = SPACE_BEFORE_PUNCT.exec(masked)) !== null) {
1963
+ const punct = m[3] ?? "";
1964
+ const after = masked[m.index + m[0].length];
1965
+ if (/^[:;]/.test(punct) && (after === ")" || after === "(")) continue;
1966
+ const spaceStart = m.index + (m[1] ?? "").length;
1967
+ const spaceEnd = spaceStart + (m[2] ?? "").length;
1968
+ out.push({
1969
+ source: "grammar/spacing",
1970
+ severity: "info",
1971
+ line: i,
1972
+ start: spaceStart,
1973
+ end: spaceEnd,
1974
+ message: `Space before "${punct}" \u2014 remove it.`,
1975
+ fix: { title: "Remove space", replacement: "" }
1976
+ });
1977
+ }
1978
+ return out;
1979
+ }
1980
+ var STANDALONE_I = /(^|[^\w'])(i)(?=$|[\s,;!?]|'[a-z])/g;
1981
+ function standaloneI(line, i) {
1982
+ const out = [];
1983
+ const masked = maskNonProse(line);
1984
+ STANDALONE_I.lastIndex = 0;
1985
+ let m;
1986
+ while ((m = STANDALONE_I.exec(masked)) !== null) {
1987
+ const start = m.index + (m[1] ?? "").length;
1988
+ out.push({
1989
+ source: "grammar/standalone-i",
1990
+ severity: "warn",
1991
+ line: i,
1992
+ start,
1993
+ end: start + 1,
1994
+ message: 'Lowercase "i" \u2014 the pronoun is always capital "I".',
1995
+ fix: { title: '\u2192 "I"', replacement: "I" }
1996
+ });
1997
+ }
1998
+ return out;
1999
+ }
2000
+ var WORD_TOKEN = /[A-Za-z][A-Za-z'’]*/g;
2001
+ function unknownWords(line, i, isWord, allow) {
2002
+ const out = [];
2003
+ const masked = maskNonProse(line);
2004
+ WORD_TOKEN.lastIndex = 0;
2005
+ let m;
2006
+ while ((m = WORD_TOKEN.exec(masked)) !== null) {
2007
+ const tok = m[0];
2008
+ if (tok.length < 3) continue;
2009
+ if (/[A-Z]/.test(tok.slice(1))) continue;
2010
+ const lower2 = tok.toLowerCase().replace(/[’']s$/, "");
2011
+ if (lower2.length < 3) continue;
2012
+ if (allow.has(lower2)) continue;
2013
+ if (Object.prototype.hasOwnProperty.call(MISSPELLINGS, lower2)) continue;
2014
+ if (isWord(tok) || isWord(lower2)) continue;
2015
+ out.push({
2016
+ source: "spelling/unknown-word",
2017
+ severity: "info",
2018
+ line: i,
2019
+ start: m.index,
2020
+ end: m.index + tok.length,
2021
+ message: `Unrecognized word \u2014 "${tok}" isn't in the dictionary. Fix the spelling, or add it to your dictionary.`
2022
+ });
2023
+ }
2024
+ return out;
2025
+ }
2026
+ var GUARD_OPENER_RE = new RegExp(`^\\s*(${keywordAlternation(GUARD_OPENERS)})\\b`, "i");
2027
+ function conditionalNeedsThen(raw, i) {
2028
+ const parsed = parseLine(raw);
2029
+ if (classifyClause(parsed.clause) !== "conditional") return [];
2030
+ const opener = parsed.clause.match(GUARD_OPENER_RE);
2031
+ if (!opener || /\bTHEN\b/i.test(parsed.clause)) return [];
2032
+ const kw = (opener[1] ?? "IF").replace(/\s+/g, " ").toUpperCase();
2033
+ const lead = raw.length - raw.trimStart().length;
2034
+ return [
2035
+ {
2036
+ source: "typeglish/if-then",
2037
+ severity: "warn",
2038
+ line: i,
2039
+ start: lead,
2040
+ end: raw.length,
2041
+ message: `${kw} needs a THEN \u2014 write ${kw} <condition> THEN <action>.`
2042
+ }
2043
+ ];
2044
+ }
2045
+ function undefinedReference(raw, i, declared, fileAliases) {
2046
+ const out = [];
2047
+ const sigil = /@\{([A-Za-z][A-Za-z0-9_.]*)\}/g;
2048
+ let m;
2049
+ while ((m = sigil.exec(raw)) !== null) {
2050
+ const name = m[1] ?? "";
2051
+ const key = name.toLowerCase();
2052
+ const base = key.split(".")[0] ?? key;
2053
+ if (fileAliases.has(base)) {
2054
+ out.push({
2055
+ source: "structure/undefined-ref",
2056
+ severity: "error",
2057
+ line: i,
2058
+ start: m.index,
2059
+ end: m.index + m[0].length,
2060
+ message: `Wrong bracket \u2014 "${base}" is a file import, and a file transcludes as a SECTION pointer. Replace @{${name}} with @<${key}>.`,
2061
+ fix: { title: `Point with @<${key}>`, replacement: `@<${key}>` }
2062
+ });
2063
+ continue;
2064
+ }
2065
+ if (declared.has(key) || declared.has(base)) continue;
2066
+ out.push({
2067
+ source: "structure/undefined-ref",
2068
+ severity: "error",
2069
+ line: i,
2070
+ start: m.index,
2071
+ end: m.index + m[0].length,
2072
+ message: `Dangling pointer \u2014 @{${name}} names nothing. Declare it with "$REQUIRE variable ${name}" (a runtime variable), or "${name} IS \u2026" (a fixed value).`
2073
+ });
2074
+ }
2075
+ const bare = /(?<!@)\{([A-Za-z][A-Za-z0-9_]*(?:[.#][A-Za-z0-9_]+)?)\}|(?<!@)\[([a-z][a-z0-9_]*(?:[.#][a-z0-9_]+)?)\]/gi;
2076
+ while ((m = bare.exec(raw)) !== null) {
2077
+ const name = m[1] ?? m[2] ?? "";
2078
+ const square = m[1] === void 0;
2079
+ const key = name.toLowerCase();
2080
+ const base = key.split(/[.#]/)[0] ?? key;
2081
+ const isFile = fileAliases.has(base);
2082
+ if (square && !isFile) continue;
2083
+ if (!square && !isFile && !declared.has(key) && !declared.has(base)) continue;
2084
+ const pointer = isFile ? `@<${key}>` : `@{${key}}`;
2085
+ out.push({
2086
+ source: "structure/retired-brace-ref",
2087
+ severity: "error",
2088
+ line: i,
2089
+ start: m.index,
2090
+ end: m.index + m[0].length,
2091
+ message: `Bare ${square ? "squares" : "braces"} are prose \u2014 they no longer reference. "${name}" is ${isFile ? "a file import" : "a declared name"}, so point at it: replace ${m[0]} with ${pointer}.`,
2092
+ fix: { title: `Point with ${pointer}`, replacement: pointer }
2093
+ });
2094
+ }
2095
+ return out;
2096
+ }
2097
+ function retiredLiteralSigil(raw, i) {
2098
+ const out = [];
2099
+ const masked = maskQuotes(raw);
2100
+ const rewriteAt = (m2) => {
2101
+ const found = raw.slice(m2.index, m2.index + m2[0].length);
2102
+ const mm = found.match(/^<(\/?)[="]([a-z][a-z0-9_]*)"?((?:\s[^>]*)?)>$/i);
2103
+ return mm ? { found, rewrite: `<${mm[1] ?? ""}"${mm[2] ?? ""}"${mm[3] ?? ""}>` } : null;
2104
+ };
2105
+ let m;
2106
+ const retired = /<\/?=[a-z][a-z0-9_]*(?:\s[^>]*)?>/gi;
2107
+ while ((m = retired.exec(masked)) !== null) {
2108
+ const r = rewriteAt(m);
2109
+ if (!r) continue;
2110
+ out.push({
2111
+ source: "structure/retired-literal-sigil",
2112
+ severity: "error",
2113
+ line: i,
2114
+ start: m.index,
2115
+ end: m.index + m[0].length,
2116
+ message: `The "=" literal-zone sigil is retired \u2014 the sigil is the quote pair now (one escape, every scale). Replace ${r.found} with ${r.rewrite}.`,
2117
+ fix: { title: `Quote it: ${r.rewrite}`, replacement: r.rewrite }
2118
+ });
2119
+ }
2120
+ const half = /<\/?"[a-z][a-z0-9_]*(?!")(?:\s[^>]*)?>/gi;
2121
+ while ((m = half.exec(masked)) !== null) {
2122
+ const r = rewriteAt(m);
2123
+ if (!r) continue;
2124
+ out.push({
2125
+ source: "structure/half-quoted-literal",
2126
+ severity: "error",
2127
+ line: i,
2128
+ start: m.index,
2129
+ end: m.index + m[0].length,
2130
+ message: `A literal section quotes its WHOLE name \u2014 both quotes, ${r.rewrite}.`,
2131
+ fix: { title: `Quote it: ${r.rewrite}`, replacement: r.rewrite }
2132
+ });
2133
+ }
2134
+ return out;
2135
+ }
2136
+ function undefinedToolInline(raw, i, defined, imported) {
2137
+ const out = [];
2138
+ const re = /@\[([a-z][a-z0-9_]*)\]/gi;
2139
+ let m;
2140
+ while ((m = re.exec(raw)) !== null) {
2141
+ const name = m[1] ?? "";
2142
+ if (defined.has(name.toLowerCase()) || imported.has(name.toLowerCase())) continue;
2143
+ out.push({
2144
+ source: "structure/undefined-tool-inline",
2145
+ severity: "error",
2146
+ line: i,
2147
+ start: m.index,
2148
+ end: m.index + m[0].length,
2149
+ message: `@[${name}] \u2014 no $TOOL or $IMPORT tool named "${name}" in this file. Define it ("$TOOL ${name}") or import it ("$IMPORT tool ${name}") so the pointer has a target.`
2150
+ });
2151
+ }
2152
+ return out;
2153
+ }
2154
+ function bareToolRef(raw, i, toolKeys) {
2155
+ if (toolKeys.size === 0) return [];
2156
+ const out = [];
2157
+ const blank = (s, re) => s.replace(re, (m2) => "_".repeat(m2.length));
2158
+ let masked = maskQuotes(stripAnnotation(stripInlineComment(raw)));
2159
+ masked = blank(masked, /@\{[^}\n]*\}|@<[^>\n]*>|@\[[^\]\n]*\]/g);
2160
+ masked = blank(masked, new RegExp(`<\\/?${TAG_NAME_SRC}(?:\\s[^>]*)?>`, "gi"));
2161
+ SNAKE_RE.lastIndex = 0;
2162
+ let m;
2163
+ while ((m = SNAKE_RE.exec(masked)) !== null) {
2164
+ const key = m[0].toLowerCase();
2165
+ if (!toolKeys.has(key)) continue;
2166
+ out.push({
2167
+ source: "structure/bare-tool-ref",
2168
+ severity: "error",
2169
+ line: i,
2170
+ start: m.index,
2171
+ end: m.index + m[0].length,
2172
+ message: `"${m[0]}" is a tool, but this mention is bare prose \u2014 the compiler cannot bind it. Point it with @[${key}] (a checked reference), or quote it ("${m[0]}") to speak the name as plain text.`,
2173
+ fix: { title: `Point with @[${key}]`, replacement: `@[${key}]` }
2174
+ });
2175
+ }
2176
+ return out;
2177
+ }
2178
+ function normItem(s) {
2179
+ return s.toLowerCase().replace(/[.,;:!?]+$/, "").replace(/\s+/g, " ").trim();
2180
+ }
2181
+ function normMember2(s) {
2182
+ return s.toLowerCase().replace(/^(?:a|an|the)\s+/, "").replace(/[.,;:!?]+$/, "").replace(/\s+/g, " ").trim();
2183
+ }
2184
+ function membersDisjoint(a, b, sets) {
2185
+ for (const ai of a) {
2186
+ const ma = normMember2(ai);
2187
+ if (ma === "") continue;
2188
+ for (const bj of b) {
2189
+ const mb = normMember2(bj);
2190
+ if (mb !== "" && ma !== mb && sets.some((s) => s.has(ma) && s.has(mb))) return true;
2191
+ }
2192
+ }
2193
+ return false;
2194
+ }
2195
+ function conflictingDeclarations(rawLines, disp = (i) => i + 1) {
2196
+ const disjointSets = [];
2197
+ for (const raw of rawLines) {
2198
+ const e = findEnumDeclaration(raw);
2199
+ if (e) disjointSets.push(new Set(e.members.map(normMember2).filter((x) => x !== "")));
2200
+ }
2201
+ if (disjointSets.length === 0) return [];
2202
+ const decls = resolveDeclarations(rawLines);
2203
+ const aliases = aliasMap(decls);
2204
+ const byName = /* @__PURE__ */ new Map();
2205
+ for (const d of decls) {
2206
+ if (d.negated || d.path || d.owns || d.items.length === 0) continue;
2207
+ const key = aliases.get(d.name.toLowerCase()) ?? d.name.toLowerCase();
2208
+ const list = byName.get(key);
2209
+ if (list) list.push({ line: d.line, items: d.items });
2210
+ else byName.set(key, [{ line: d.line, items: d.items }]);
2211
+ }
2212
+ const out = [];
2213
+ const seen = /* @__PURE__ */ new Set();
2214
+ for (const decls2 of byName.values()) {
2215
+ for (let a = 0; a < decls2.length; a += 1) {
2216
+ for (let b = a + 1; b < decls2.length; b += 1) {
2217
+ const da = decls2[a];
2218
+ const db = decls2[b];
2219
+ if (!da || !db || !membersDisjoint(da.items, db.items, disjointSets)) continue;
2220
+ for (const [self, peer] of [
2221
+ [da, db],
2222
+ [db, da]
2223
+ ]) {
2224
+ const dedupe = `${self.line}|${peer.line}`;
2225
+ if (seen.has(dedupe)) continue;
2226
+ seen.add(dedupe);
2227
+ const raw = rawLines[self.line] ?? "";
2228
+ const lead = raw.length - raw.trimStart().length;
2229
+ out.push({
2230
+ source: "logic/redeclaration",
2231
+ severity: "error",
2232
+ line: self.line,
2233
+ start: lead,
2234
+ end: raw.length,
2235
+ message: `Mutually-exclusive values \u2014 this line asserts "${self.items.join(", ")}" while line ${disp(peer.line)} asserts "${peer.items.join(", ")}", and both belong to one closed "one of" set (an object holds exactly ONE member). Keep one assertion, or widen the declared set.`
2236
+ });
2237
+ }
2238
+ }
2239
+ }
2240
+ }
2241
+ return out;
2242
+ }
2243
+ function declarationRedundancy(rawLines, disp = (i) => i + 1) {
2244
+ const decls = resolveDeclarations(rawLines);
2245
+ const aliases = aliasMap(decls);
2246
+ const canon = (name) => aliases.get(name.toLowerCase()) ?? name.toLowerCase();
2247
+ const byName = /* @__PURE__ */ new Map();
2248
+ for (const d of decls) {
2249
+ if (d.negated || d.path || d.owns) continue;
2250
+ const items = new Set(d.items.map(normItem).filter((x) => x !== ""));
2251
+ if (items.size === 0) continue;
2252
+ const key = canon(d.name);
2253
+ const list = byName.get(key);
2254
+ if (list) list.push({ line: d.line, items });
2255
+ else byName.set(key, [{ line: d.line, items }]);
2256
+ }
2257
+ const out = [];
2258
+ const flagged = /* @__PURE__ */ new Set();
2259
+ for (const [name, entries] of byName) {
2260
+ if (entries.length < 2) continue;
2261
+ for (const d of entries) {
2262
+ if (flagged.has(d.line)) continue;
2263
+ const cover = entries.find(
2264
+ (e) => e.line !== d.line && [...d.items].every((it) => e.items.has(it)) && (d.items.size < e.items.size || e.line < d.line)
2265
+ );
2266
+ if (!cover) continue;
2267
+ flagged.add(d.line);
2268
+ const raw = rawLines[d.line] ?? "";
2269
+ const lead = raw.length - raw.trimStart().length;
2270
+ const identical = d.items.size === cover.items.size;
2271
+ out.push({
2272
+ source: "clarity/duplicate",
2273
+ severity: "warn",
2274
+ line: d.line,
2275
+ start: lead,
2276
+ end: raw.length,
2277
+ message: identical ? `Duplicate declaration \u2014 "${name}" is already declared identically on line ${disp(cover.line)}. Remove this one.` : `Redundant declaration \u2014 everything "${name}" asserts here is already covered by line ${disp(cover.line)}. Merge or remove it.`
2278
+ });
2279
+ }
2280
+ }
2281
+ return out;
2282
+ }
2283
+ function negatedFactConflicts(rawLines, disp = (i) => i + 1) {
2284
+ const decls = resolveDeclarations(rawLines);
2285
+ const aliases = aliasMap(decls);
2286
+ const canon = (name) => aliases.get(name.toLowerCase()) ?? name.toLowerCase();
2287
+ const facts = /* @__PURE__ */ new Map();
2288
+ for (const d of decls) {
2289
+ if (d.path || d.owns) continue;
2290
+ for (const rawItem of d.items) {
2291
+ const item = normItem(rawItem);
2292
+ if (item === "") continue;
2293
+ const key = `${canon(d.name)}|${item}`;
2294
+ const rec = facts.get(key) ?? { name: d.name, item: rawItem.trim(), pos: [], neg: [] };
2295
+ (d.negated ? rec.neg : rec.pos).push(d.line);
2296
+ facts.set(key, rec);
2297
+ }
2298
+ }
2299
+ const out = [];
2300
+ for (const rec of facts.values()) {
2301
+ if (rec.pos.length === 0 || rec.neg.length === 0) continue;
2302
+ const posLine = rec.pos[0] ?? 0;
2303
+ const negLine = rec.neg[0] ?? 0;
2304
+ for (const ln of [posLine, negLine]) {
2305
+ const raw = rawLines[ln] ?? "";
2306
+ const lead = raw.length - raw.trimStart().length;
2307
+ const other = ln === posLine ? negLine : posLine;
2308
+ out.push({
2309
+ source: "logic/contradiction",
2310
+ severity: "error",
2311
+ line: ln,
2312
+ start: lead,
2313
+ end: raw.length,
2314
+ message: `Contradiction \u2014 "${rec.name}" is asserted to be and not be "${rec.item}" (line ${disp(other)}).`
2315
+ });
2316
+ }
2317
+ }
2318
+ return out;
2319
+ }
2320
+ function propertyConflicts(rawLines) {
2321
+ const decls = resolveDeclarations(rawLines);
2322
+ const aliases = aliasMap(decls);
2323
+ const canon = (name) => aliases.get(name.toLowerCase()) ?? name.toLowerCase();
2324
+ const fields = /* @__PURE__ */ new Map();
2325
+ for (const d of decls) {
2326
+ if (!d.path) continue;
2327
+ const key = `${canon(d.name)}|${d.path.join(".")}`;
2328
+ const rec = fields.get(key) ?? { cardinality: "one", pos: /* @__PURE__ */ new Map(), neg: /* @__PURE__ */ new Map() };
2329
+ if (d.cardinality === "many") rec.cardinality = "many";
2330
+ for (const rawItem of d.items) {
2331
+ const item = normItem(rawItem);
2332
+ if (item === "") continue;
2333
+ const bucket = d.negated ? rec.neg : rec.pos;
2334
+ bucket.set(item, [...bucket.get(item) ?? [], d.line]);
2335
+ }
2336
+ fields.set(key, rec);
2337
+ }
2338
+ const out = [];
2339
+ const seen = /* @__PURE__ */ new Set();
2340
+ const flag = (lines, message) => {
2341
+ for (const ln of lines) {
2342
+ if (seen.has(ln)) continue;
2343
+ seen.add(ln);
2344
+ const raw = rawLines[ln] ?? "";
2345
+ const lead = raw.length - raw.trimStart().length;
2346
+ out.push({ source: "logic/contradiction", severity: "error", line: ln, start: lead, end: raw.length, message });
2347
+ }
2348
+ };
2349
+ for (const [key, rec] of fields) {
2350
+ const label = key.replace("|", ".");
2351
+ for (const [item, posLines] of rec.pos) {
2352
+ const negLines = rec.neg.get(item);
2353
+ if (negLines && negLines.length > 0) {
2354
+ flag([posLines[0] ?? 0, negLines[0] ?? 0], `Contradiction \u2014 "${label}" is both assigned and denied the value "${item}".`);
2355
+ }
2356
+ }
2357
+ if (rec.cardinality === "one" && rec.pos.size >= 2) {
2358
+ const vals = [...rec.pos.keys()].map((v) => `"${v}"`).join(", ");
2359
+ flag(
2360
+ [...rec.pos.values()].map((ls) => ls[0] ?? 0),
2361
+ `Contradiction \u2014 single-valued "${label}" is assigned ${rec.pos.size} different values (${vals}).`
2362
+ );
2363
+ }
2364
+ }
2365
+ return out;
2366
+ }
2367
+ function ownershipConflicts(rawLines) {
2368
+ const decls = resolveDeclarations(rawLines);
2369
+ const aliases = aliasMap(decls);
2370
+ const canon = (name) => aliases.get(name.toLowerCase()) ?? name.toLowerCase();
2371
+ const things = /* @__PURE__ */ new Map();
2372
+ for (const d of decls) {
2373
+ if (!d.owns) continue;
2374
+ const thing = normMember2(d.owns);
2375
+ if (thing === "") continue;
2376
+ const owner = canon(d.name);
2377
+ const owners = things.get(thing) ?? /* @__PURE__ */ new Map();
2378
+ owners.set(owner, [...owners.get(owner) ?? [], d.line]);
2379
+ things.set(thing, owners);
2380
+ }
2381
+ const out = [];
2382
+ const seen = /* @__PURE__ */ new Set();
2383
+ for (const [thing, owners] of things) {
2384
+ if (owners.size < 2) continue;
2385
+ const names = [...owners.keys()].join(", ");
2386
+ for (const ln of [...owners.values()].flat().sort((a, b) => a - b)) {
2387
+ if (seen.has(ln)) continue;
2388
+ seen.add(ln);
2389
+ const raw = rawLines[ln] ?? "";
2390
+ const lead = raw.length - raw.trimStart().length;
2391
+ out.push({
2392
+ source: "logic/contradiction",
2393
+ severity: "error",
2394
+ line: ln,
2395
+ start: lead,
2396
+ end: raw.length,
2397
+ message: `Contradiction \u2014 "${thing}" is owned by ${owners.size} different owners (${names}).`
2398
+ });
2399
+ }
2400
+ }
2401
+ return out;
2402
+ }
2403
+ function runDiagnostics(rawLines, _rubric, opts = {}) {
2404
+ const render = renderLines(rawLines);
2405
+ const toolMask = toolBlockMask(rawLines);
2406
+ const exMask = examplesMask(rawLines);
2407
+ const defMask = defListMask(rawLines);
2408
+ const zoneMask = illustrationZoneMask(rawLines);
2409
+ const scoped = rawLines.map((l, i) => toolMask[i] || exMask[i] || TAG_ONLY_LINE.test(l) ? "" : stripAnnotation(l));
2410
+ const addressees = addresseeSubjects(scoped, opts.addressees ?? []);
2411
+ const ordinals = lineOrdinals(rawLines);
2412
+ const disp = (i) => displayLine(ordinals, i);
2413
+ const doc = buildStructure(rawLines);
2414
+ const parsed = parseTools(rawLines);
2415
+ const definedTools = parsed.tools;
2416
+ const toolDefKeys = new Set(definedTools.map((t) => t.name.toLowerCase()));
2417
+ const importedToolKeys = new Set(doc.imports.filter((im) => im.kind === "tool").map((im) => im.key));
2418
+ const allToolKeys = /* @__PURE__ */ new Set([...toolDefKeys, ...importedToolKeys]);
2419
+ const declared = /* @__PURE__ */ new Set();
2420
+ const decls = resolveDeclarations(rawLines);
2421
+ for (const d of decls) {
2422
+ if (!d.negated) declared.add(d.name.toLowerCase());
2423
+ }
2424
+ for (const aliasKey of aliasMap(decls).keys()) declared.add(aliasKey);
2425
+ for (const im of doc.imports) if (im.kind === "variable") declared.add(im.key);
2426
+ for (const name of doc.defines.keys()) declared.add(name);
2427
+ const fileAliases = new Set(doc.fileImports.map((f) => f.key));
2428
+ const allow = /* @__PURE__ */ new Set();
2429
+ const addAllow = (s) => {
2430
+ for (const part of s.toLowerCase().match(/[a-z][a-z'’]*/g) ?? []) if (part.length >= 3) allow.add(part);
2431
+ };
2432
+ for (const name of declared) addAllow(name);
2433
+ let importLines = /* @__PURE__ */ new Set();
2434
+ if (opts.isWord) {
2435
+ importLines = /* @__PURE__ */ new Set([...doc.importLines, ...doc.configLines]);
2436
+ for (const im of doc.imports) addAllow(im.name);
2437
+ for (const base of doc.variables.keys()) addAllow(base);
2438
+ for (const anchor of doc.index.anchors) addAllow(anchor);
2439
+ for (const t of definedTools) {
2440
+ addAllow(t.name);
2441
+ for (const p of t.params) addAllow(p.name);
2442
+ }
2443
+ }
2444
+ const out = [];
2445
+ const exempt = proseExemptLines(rawLines);
2446
+ const testLines = testBlockLines(rawLines);
2447
+ const exampleLines = exampleBlockLines(rawLines);
2448
+ const cfgMask = configSectionMask(rawLines);
2449
+ const chainArms = new Set(parseSwitches(rawLines).flatMap((b) => b.error === null ? b.arms.map((a) => a.headerLine) : []));
2450
+ for (let i = 0; i < rawLines.length; i += 1) {
2451
+ const line = rawLines[i] ?? "";
2452
+ out.push(...leakedSecrets(line, i));
2453
+ if (testLines.has(i) || exampleLines.has(i) || cfgMask[i] && !toolMask[i]) continue;
2454
+ const r = render[i];
2455
+ if (!r || r.kind !== "content" || r.lineClass === "md-code" || r.lineClass === "md-heading") {
2456
+ continue;
2457
+ }
2458
+ out.push(
2459
+ ...wordy(line, i),
2460
+ ...hedging(line, i),
2461
+ ...vague(line, i),
2462
+ ...doubleWord(line, i),
2463
+ ...spelling(line, i),
2464
+ ...spacing(line, i),
2465
+ ...standaloneI(line, i),
2466
+ ...toolMask[i] || chainArms.has(i) ? [] : conditionalNeedsThen(line, i),
2467
+ ...toolMask[i] ? [] : quotedProse(line, i),
2468
+ ...toolMask[i] ? [] : specialCharacters(line, i),
2469
+ ...trailingSpace(line, i),
2470
+ ...tabCharacters(line, i),
2471
+ ...phantomColumns(line, i, defMask[i] === true || toolMask[i] === true),
2472
+ ...defMask[i] || toolMask[i] || chainArms.has(i) || exempt.has(i) || isAnnotationLine(line) || line.trimStart().startsWith("$") || TAG_ONLY_LINE.test(line) || matchSectionTag(line) !== null ? [] : missingPeriod(line, i),
2473
+ ...oneOfColon(line, i),
2474
+ ...toolMask[i] || zoneMask[i] ? [] : inlineMarker(line, i),
2475
+ ...toolMask[i] ? [] : undefinedReference(line, i, declared, fileAliases),
2476
+ ...toolMask[i] || zoneMask[i] ? [] : retiredLiteralSigil(line, i),
2477
+ ...toolMask[i] ? [] : undefinedToolInline(line, i, toolDefKeys, importedToolKeys),
2478
+ ...toolMask[i] || isAnnotationLine(line) || line.trimStart().startsWith("$") ? [] : bareToolRef(line, i, allToolKeys),
2479
+ ...exempt.has(i) ? [] : injectionPatterns(line, i),
2480
+ ...opts.isWord && !importLines.has(i) ? unknownWords(line, i, opts.isWord, allow) : []
2481
+ );
2482
+ }
2483
+ for (const e of defEntries(rawLines)) {
2484
+ if (zoneMask[e.line] || toolMask[e.line] || testLines.has(e.line) || exampleLines.has(e.line)) continue;
2485
+ let fence = false;
2486
+ for (const li of e.lines) {
2487
+ const raw = rawLines[li] ?? "";
2488
+ const t = raw.trim();
2489
+ if (fence) {
2490
+ if (t.startsWith("```") || t.startsWith("~~~")) fence = false;
2491
+ continue;
2492
+ }
2493
+ if (t.startsWith("```") || t.startsWith("~~~")) {
2494
+ fence = true;
2495
+ continue;
2496
+ }
2497
+ if (t === "") continue;
2498
+ if (li === e.line || t.startsWith("- ")) {
2499
+ out.push(...missingPeriodLiteral(raw, li));
2500
+ continue;
2501
+ }
2502
+ const lead = raw.length - raw.trimStart().length;
2503
+ out.push({
2504
+ source: "structure/wrapped-definition",
2505
+ severity: "error",
2506
+ line: li,
2507
+ start: lead,
2508
+ end: raw.length,
2509
+ message: 'One statement per line: this hard-wrapped continuation belongs to the statement above. Merge it into that line (the editor wraps long lines), or make it its own "- " sub-item.'
2510
+ });
2511
+ }
2512
+ }
2513
+ {
2514
+ const spec = layoutColumns(rawLines);
2515
+ for (let i = 0; i < rawLines.length; i += 1) {
2516
+ const raw = rawLines[i] ?? "";
2517
+ if (raw.trim() === "") continue;
2518
+ const expected = spec.col[i];
2519
+ if (expected === null || expected === void 0) continue;
2520
+ const actual = (raw.match(/^ */) ?? [""])[0].length;
2521
+ if (actual === expected || spec.substep[i] === true && actual === expected + 2) continue;
2522
+ const depthLabel = expected / 2;
2523
+ out.push({
2524
+ source: "structure/bad-indent",
2525
+ severity: "error",
2526
+ line: i,
2527
+ start: 0,
2528
+ end: actual,
2529
+ message: `Indentation mirrors section nesting: expected column ${expected} (${depthLabel} level${depthLabel === 1 ? "" : "s"} deep), found ${actual}. The indent unit is 2 spaces.`,
2530
+ fix: { title: `\u2192 col ${expected}`, replacement: " ".repeat(expected) }
2531
+ });
2532
+ }
2533
+ }
2534
+ out.push(...wrappedFragments(rawLines, disp));
2535
+ out.push(...unmeasurableDirectives(scoped, exempt));
2536
+ out.push(...untestedRules(scoped, testLines));
2537
+ out.push(...determinableExpects(rawLines));
2538
+ out.push(...conflictingDeclarations(scoped, disp));
2539
+ out.push(...declarationRedundancy(scoped, disp));
2540
+ out.push(...negatedFactConflicts(scoped, disp));
2541
+ out.push(...propertyConflicts(scoped));
2542
+ out.push(...ownershipConflicts(scoped));
2543
+ const ANTONYM_CONFLICT_SEVERITY = "error";
2544
+ for (const conflict of checkConsistency(scoped, addressees)) {
2545
+ for (const ln of conflict.lines) {
2546
+ const raw = rawLines[ln] ?? "";
2547
+ const lead = raw.length - raw.trimStart().length;
2548
+ const other = conflict.lines.find((x) => x !== ln);
2549
+ out.push({
2550
+ source: conflict.strength === true ? "logic/quantifier-strength" : "logic/contradiction",
2551
+ severity: conflict.viaAntonym === true ? ANTONYM_CONFLICT_SEVERITY : "error",
2552
+ line: ln,
2553
+ start: lead,
2554
+ end: raw.length,
2555
+ message: other !== void 0 ? `${conflict.message} (conflicts with line ${disp(other)})` : conflict.message
2556
+ });
2557
+ }
2558
+ }
2559
+ for (const conflict of checkOrder(scoped)) {
2560
+ for (const ln of conflict.lines) {
2561
+ const raw = rawLines[ln] ?? "";
2562
+ const lead = raw.length - raw.trimStart().length;
2563
+ const others = conflict.lines.filter((x) => x !== ln).map((x) => disp(x));
2564
+ out.push({
2565
+ source: "logic/order",
2566
+ severity: "error",
2567
+ line: ln,
2568
+ start: lead,
2569
+ end: raw.length,
2570
+ message: others.length > 0 ? `${conflict.message} (the loop runs through line${others.length > 1 ? "s" : ""} ${others.join(", ")})` : conflict.message
2571
+ });
2572
+ }
2573
+ }
2574
+ for (const conflict of checkQuantifier(scoped)) {
2575
+ for (const ln of conflict.lines) {
2576
+ const raw = rawLines[ln] ?? "";
2577
+ const lead = raw.length - raw.trimStart().length;
2578
+ const other = conflict.lines.find((x) => x !== ln);
2579
+ const message = other !== void 0 ? `${conflict.message} (conflicts with line ${disp(other)})` : conflict.message;
2580
+ const base = { severity: "error", line: ln, start: lead, end: raw.length, message };
2581
+ if (conflict.kind === "exclusivity") out.push({ source: "logic/exclusivity", ...base });
2582
+ else if (conflict.kind === "majority") out.push({ source: "logic/majority", ...base });
2583
+ else if (conflict.kind === "strength") out.push({ source: "logic/quantifier-strength", ...base });
2584
+ else if (conflict.kind === "cardinality") out.push({ source: "logic/cardinality", ...base });
2585
+ else if (conflict.kind === "action-count") out.push({ source: "logic/action-count", ...base });
2586
+ else if (conflict.kind === "insufficient-set") out.push({ source: "logic/insufficient-set", ...base });
2587
+ else out.push({ source: "logic/quantifier", ...base });
2588
+ }
2589
+ }
2590
+ for (const conflict of checkSyllogism(scoped)) {
2591
+ for (const ln of conflict.lines) {
2592
+ const raw = rawLines[ln] ?? "";
2593
+ const lead = raw.length - raw.trimStart().length;
2594
+ const others = conflict.lines.filter((x) => x !== ln).map((x) => disp(x));
2595
+ const message = others.length > 0 ? `${conflict.message} (with lines ${others.join(", ")})` : conflict.message;
2596
+ out.push({ source: "logic/syllogism", severity: "error", line: ln, start: lead, end: raw.length, message });
2597
+ }
2598
+ }
2599
+ for (const conflict of checkDefiniteness(rawLines)) {
2600
+ for (const ln of conflict.lines) {
2601
+ const raw = rawLines[ln] ?? "";
2602
+ const lead = raw.length - raw.trimStart().length;
2603
+ const other = conflict.lines.find((x) => x !== ln);
2604
+ const message = other !== void 0 ? `${conflict.message} (also line ${disp(other)})` : conflict.message;
2605
+ const base = { severity: "error", line: ln, start: lead, end: raw.length, message };
2606
+ if (conflict.code === "definite-ambiguous") out.push({ source: "logic/definite-ambiguous", ...base });
2607
+ else out.push({ source: "logic/definite-undeclared", ...base });
2608
+ }
2609
+ }
2610
+ for (const c of structuralConflicts(rawLines)) {
2611
+ for (const ln of c.lines) {
2612
+ const raw = rawLines[ln] ?? "";
2613
+ const lead = raw.length - raw.trimStart().length;
2614
+ const other = c.lines.find((x) => x !== ln);
2615
+ out.push({
2616
+ source: `structure/${c.code}`,
2617
+ severity: c.severity,
2618
+ line: ln,
2619
+ start: lead,
2620
+ end: raw.length,
2621
+ message: other !== void 0 ? `${c.message} (conflicts with line ${disp(other)})` : c.message
2622
+ });
2623
+ }
2624
+ }
2625
+ const configSchema = opts.glishConfig ? resolveConfigSchema(opts.glishConfig) : BUILTIN_CONFIG_SCHEMA;
2626
+ const configRules = opts.glishConfig ? parseConfigRules(opts.glishConfig) : [];
2627
+ const serviceNames = new Set(parsed.services.map((s) => s.name));
2628
+ for (const f of [...configParamDiagnostics(rawLines, configSchema, serviceNames), ...configRuleDiagnostics(rawLines, configRules)]) {
2629
+ out.push({ source: f.code, severity: f.severity, line: f.line, start: f.start, end: f.end, message: f.message });
2630
+ }
2631
+ for (const ln of deadRules(scoped)) {
2632
+ const raw = rawLines[ln] ?? "";
2633
+ const lead = raw.length - raw.trimStart().length;
2634
+ out.push({
2635
+ source: "clarity/dead-rule",
2636
+ severity: "warn",
2637
+ line: ln,
2638
+ start: lead,
2639
+ end: raw.length,
2640
+ message: "Dead rule \u2014 the IF-condition contradicts ITSELF (the shape of `IF c AND NOT c`), so it can never hold and the rule never fires. Fix the condition's logic, or remove the rule."
2641
+ });
2642
+ }
2643
+ for (const { line, start } of looseContentLines(rawLines)) {
2644
+ out.push({
2645
+ source: "structure/loose-content",
2646
+ severity: "warn",
2647
+ line,
2648
+ start,
2649
+ end: (rawLines[line] ?? "").length,
2650
+ message: "This content is outside any section \u2014 put it inside a <tag> (a heading ends where a tag or the next heading begins)."
2651
+ });
2652
+ }
2653
+ {
2654
+ const entries = defEntries(rawLines);
2655
+ const seen = /* @__PURE__ */ new Map();
2656
+ for (let k = 0; k < entries.length; k += 1) {
2657
+ const e = entries[k];
2658
+ if (!e) continue;
2659
+ const prev = entries[k - 1];
2660
+ const contiguous = prev !== void 0 && e.line === (prev.lines[prev.lines.length - 1] ?? prev.line) + 1;
2661
+ if (!contiguous) seen.clear();
2662
+ const first = seen.get(e.term);
2663
+ if (first !== void 0) {
2664
+ out.push({
2665
+ source: "structure/duplicate-term",
2666
+ severity: "warn",
2667
+ line: e.line,
2668
+ start: e.termCol,
2669
+ end: e.termCol + e.term.length + 2,
2670
+ message: `Duplicate term "${e.term}" \u2014 this list already defines it on line ${disp(first)}.`
2671
+ });
2672
+ } else {
2673
+ seen.set(e.term, e.line);
2674
+ }
2675
+ }
2676
+ }
2677
+ for (const { line, start } of headingsOutsideSections(rawLines)) {
2678
+ out.push({
2679
+ source: "structure/heading-outside-section",
2680
+ severity: "warn",
2681
+ line,
2682
+ start,
2683
+ end: (rawLines[line] ?? "").length,
2684
+ message: "This heading is outside every <tag> section \u2014 when a prompt uses XML sections, headings must live inside them."
2685
+ });
2686
+ }
2687
+ {
2688
+ const lineKind = (raw) => {
2689
+ if (raw === void 0) return "the end of the file";
2690
+ const t = raw.trim();
2691
+ if (t === "") return "a blank line";
2692
+ if (t.startsWith("//") || t.startsWith("/*")) return "a comment";
2693
+ if (t.startsWith("```") || t.startsWith("~~~")) return "a code fence";
2694
+ if (TAG_ONLY_LINE.test(raw)) return "a section tag";
2695
+ if (t.startsWith("#")) return "a heading";
2696
+ if (/^\$(?:IF|ELSE|SWITCH)\b/i.test(t)) return null;
2697
+ if (t.startsWith("$")) return "a compiler command";
2698
+ if (stripAnnotation(stripInlineComment(raw)).trim() === "") return "stripped metadata";
2699
+ return null;
2700
+ };
2701
+ let inFence = false;
2702
+ for (let i = 0; i < rawLines.length; i += 1) {
2703
+ const raw = rawLines[i] ?? "";
2704
+ const t = raw.trim();
2705
+ if (t.startsWith("```") || t.startsWith("~~~")) {
2706
+ inFence = !inFence;
2707
+ continue;
2708
+ }
2709
+ if (inFence) continue;
2710
+ if (exMask[i]) continue;
2711
+ if (isAnnotationLine(raw)) {
2712
+ const lead = raw.length - raw.trimStart().length;
2713
+ const next = rawLines[i + 1];
2714
+ if (next !== void 0 && isAnnotationLine(next)) {
2715
+ out.push({
2716
+ source: "structure/stacked-annotation",
2717
+ severity: "error",
2718
+ line: i,
2719
+ start: lead,
2720
+ end: raw.length,
2721
+ message: "Stacked annotations: a `@@` note documents the ONE statement directly below it, and the line below is another `@@`. Merge the notes into one line, or give each note its own statement."
2722
+ });
2723
+ } else {
2724
+ const kind = lineKind(next);
2725
+ if (kind !== null) {
2726
+ out.push({
2727
+ source: "structure/unattached-annotation",
2728
+ severity: "error",
2729
+ line: i,
2730
+ start: lead,
2731
+ end: raw.length,
2732
+ message: `A \`@@\` annotation documents the statement DIRECTLY below it \u2014 here the next line is ${kind}. Move the note to the line immediately above its statement.`
2733
+ });
2734
+ }
2735
+ }
2736
+ continue;
2737
+ }
2738
+ if (hasTrailingAnnotation(raw)) {
2739
+ const at = raw.indexOf("@@");
2740
+ out.push({
2741
+ source: "structure/misplaced-annotation",
2742
+ severity: "warn",
2743
+ line: i,
2744
+ start: at < 0 ? 0 : at,
2745
+ end: raw.length,
2746
+ message: "A `@@` statement annotation belongs on its own line, before the statement it documents \u2014 move it above."
2747
+ });
2748
+ }
2749
+ }
2750
+ }
2751
+ for (const group of checkRedundancy(scoped, addressees)) {
2752
+ const force = group.kind === "obligate" ? "required" : "forbidden";
2753
+ for (const ln of group.lines) {
2754
+ const raw = rawLines[ln] ?? "";
2755
+ const lead = raw.length - raw.trimStart().length;
2756
+ const others = group.lines.filter((x) => x !== ln).map((x) => x + 1);
2757
+ out.push({
2758
+ source: "clarity/duplicate",
2759
+ severity: "warn",
2760
+ line: ln,
2761
+ start: lead,
2762
+ end: raw.length,
2763
+ message: `Duplicate rule \u2014 "${group.atom}" is ${force} in ${group.lines.length} places (also ${others.length > 1 ? "lines" : "line"} ${others.join(", ")}). State it once and reference it.`
2764
+ });
2765
+ }
2766
+ }
2767
+ for (const s of checkSubsumption(scoped, addressees)) {
2768
+ const raw = rawLines[s.redundantLine] ?? "";
2769
+ const lead = raw.length - raw.trimStart().length;
2770
+ const force = s.kind === "obligate" ? "required" : "forbidden";
2771
+ out.push({
2772
+ source: "clarity/duplicate",
2773
+ severity: "warn",
2774
+ line: s.redundantLine,
2775
+ start: lead,
2776
+ end: raw.length,
2777
+ message: `Subsumed rule \u2014 "${s.atom}" is already ${force} by a broader rule on line ${disp(
2778
+ s.coveringLine
2779
+ )}, so this narrower condition never changes the outcome. Drop the condition or remove this line.`
2780
+ });
2781
+ }
2782
+ for (const f of checkModel(rawLines)) {
2783
+ out.push({ source: "config/unknown-model", severity: f.severity, line: f.line, start: f.start, end: f.end, message: f.message });
2784
+ }
2785
+ const noteAt = rawLines.map((l) => {
2786
+ if (isAnnotationLine(l)) return 0;
2787
+ const m = l.match(/(^|\s)@@/);
2788
+ return m && m.index !== void 0 ? m.index + (m[1] ?? "").length : -1;
2789
+ });
2790
+ const isAnnotationDiag = (src) => src === "structure/misplaced-annotation" || src === "structure/unattached-annotation" || src === "structure/stacked-annotation";
2791
+ const judgesConstruct = (src) => src === "structure/duplicate-term";
2792
+ const contentRuleInDefs = (src) => src === "structure/missing-period" || src === "structure/wrapped-definition" || src === "structure/em-dash" || src === "structure/curly-quote" || src === "structure/special-character" || src === "structure/inline-marker" || src === "structure/bad-indent" || src === "structure/phantom-column" || src === "structure/trailing-space" || src === "structure/tab";
2793
+ return out.filter((d) => {
2794
+ if (!isAnnotationDiag(d.source)) {
2795
+ const a = noteAt[d.line] ?? -1;
2796
+ if (a >= 0 && d.start >= a) return false;
2797
+ }
2798
+ return !exMask[d.line] || d.source.startsWith("spelling/") || d.source.startsWith("security/") || // The determinable-expect nudge ONLY ever lands on a $TEST `- expect::` sub-bullet (that is all
2799
+ // rubricExpectLines yields), and those rows are def-SHAPED, so examplesMask marks them literal.
2800
+ // It is a control-plane nudge, not example content — it survives the literal-zone sweep by
2801
+ // construction. (Before the bullet refactor, `EXPECT …` wasn't def-shaped, so it slipped past.)
2802
+ d.source === "prompt/determinable-expect" || judgesConstruct(d.source) || defMask[d.line] === true && zoneMask[d.line] !== true && contentRuleInDefs(d.source);
2803
+ });
2804
+ }
2805
+ function applyFix(value, d) {
2806
+ if (!d.fix) return value;
2807
+ const lines = value.split("\n");
2808
+ const line = lines[d.line];
2809
+ if (line === void 0) return value;
2810
+ lines[d.line] = line.slice(0, d.start) + d.fix.replacement + line.slice(d.end);
2811
+ return lines.join("\n");
2812
+ }
2813
+ function composeSpans(tokens2, diags) {
2814
+ const text = tokens2.map((t) => t.text).join("");
2815
+ const n = text.length;
2816
+ const mdCls = [];
2817
+ for (const t of tokens2) for (let k2 = 0; k2 < t.text.length; k2 += 1) mdCls.push(t.cls);
2818
+ const sev = new Array(n).fill(void 0);
2819
+ for (const d of diags) {
2820
+ for (let k2 = Math.max(0, d.start); k2 < Math.min(n, d.end); k2 += 1) {
2821
+ const cur = sev[k2];
2822
+ if (!cur || SEV_RANK[d.severity] > SEV_RANK[cur]) sev[k2] = d.severity;
2823
+ }
2824
+ }
2825
+ const spans = [];
2826
+ let k = 0;
2827
+ while (k < n) {
2828
+ const md = mdCls[k];
2829
+ const s = sev[k];
2830
+ let j = k + 1;
2831
+ while (j < n && mdCls[j] === md && sev[j] === s) j += 1;
2832
+ const cls = [md, s ? `diag-${s}` : void 0].filter(Boolean).join(" ") || void 0;
2833
+ spans.push({ text: text.slice(k, j), cls });
2834
+ k = j;
2835
+ }
2836
+ return spans;
2837
+ }
2838
+
2839
+ // src/core/catalog.ts
2840
+ var RANK = { critical: 3, important: 2, advisory: 1 };
2841
+ function atLeastImportant(a, b) {
2842
+ return RANK[a] >= RANK[b];
2843
+ }
2844
+ var CORRECTNESS = /* @__PURE__ */ new Set(["structure", "logic", "security", "config", "typeglish", "clarity"]);
2845
+ var OVERRIDES = {};
2846
+ function categoryOf(code) {
2847
+ const i = code.indexOf("/");
2848
+ return i < 0 ? code : code.slice(0, i);
2849
+ }
2850
+ function importanceOf(code, severity) {
2851
+ const override = OVERRIDES[code];
2852
+ if (override) return override;
2853
+ if (severity === "error") return "critical";
2854
+ return CORRECTNESS.has(categoryOf(code)) ? "important" : "advisory";
2855
+ }
2856
+
2857
+ // src/core/edits.ts
2858
+ var IDENT = /^[a-z][a-z0-9_]*$/i;
2859
+ var RESERVED = /* @__PURE__ */ new Set(["not", "and", "or", "is", "one", "of"]);
2860
+ var escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2861
+ var fail = (source, error) => ({ ok: false, source, changed: 0, error });
2862
+ function rename(source, kind, from, to) {
2863
+ const lines = source.split("\n");
2864
+ const doc = buildStructure(lines);
2865
+ const fromKey = from.toLowerCase();
2866
+ const toKey = to.toLowerCase();
2867
+ if (!IDENT.test(to)) return fail(source, `"${to}" isn't a valid name \u2014 use letters, digits, and underscore, starting with a letter.`);
2868
+ if (RESERVED.has(toKey)) return fail(source, `"${to}" is a reserved word (${[...RESERVED].join(", ")}) \u2014 it would reparse as an operator.`);
2869
+ const declLine = kind === "define" ? doc.defines.get(fromKey)?.line : doc.imports.find((im) => im.kind === "variable" && im.key === fromKey)?.line;
2870
+ if (declLine === void 0) return fail(source, `no ${kind === "define" ? "$DEFINE" : "$REQUIRE variable"} named "${from}".`);
2871
+ if (doc.index.inputs.has(toKey) || doc.defines.has(toKey)) return fail(source, `"${to}" is already declared \u2014 pick a name that's free.`);
2872
+ if (fromKey === toKey) return fail(source, `"${to}" is already the name \u2014 nothing to rename.`);
2873
+ const toolMask = toolBlockMask(lines);
2874
+ const refPattern = new RegExp(`@\\{${escapeRegex(fromKey)}\\}|\\$${escapeRegex(fromKey)}\\b`, "gi");
2875
+ let changed = 0;
2876
+ const out = lines.map((line, i) => {
2877
+ let s = line;
2878
+ if (i === declLine) {
2879
+ if (kind === "define") {
2880
+ s = s.replace(/^(\s*\$DEFINE\s+)([a-z][a-z0-9_]*)/i, (m, pre, name) => name.toLowerCase() === fromKey ? (changed += 1, `${pre}${to}`) : m);
2881
+ } else {
2882
+ s = s.replace(/^(\s*(?:\$REQUIRE\s+variable|\$IMPORT\s+input)\s+)([^:]*)/i, (_m, pre, names) => `${pre}${names.replace(new RegExp(`\\b${escapeRegex(fromKey)}\\b`, "i"), () => (changed += 1, to))}`);
2883
+ }
2884
+ }
2885
+ if (!toolMask[i]) {
2886
+ const safe = maskQuotes(s).replace(/\s+\/\/.*$/, (c) => " ".repeat(c.length));
2887
+ const hits = [...safe.matchAll(refPattern)].map((m) => ({ idx: m.index ?? 0, len: m[0].length }));
2888
+ for (const h of hits.reverse()) {
2889
+ s = `${s.slice(0, h.idx)}@{${to}}${s.slice(h.idx + h.len)}`;
2890
+ changed += 1;
2891
+ }
2892
+ }
2893
+ return s;
2894
+ });
2895
+ return { ok: true, source: out.join("\n"), changed };
2896
+ }
2897
+
2898
+ // src/core/semantic/atoms.ts
2899
+ function fnv32(s, seed) {
2900
+ let h = seed >>> 0;
2901
+ for (let i = 0; i < s.length; i += 1) {
2902
+ h ^= s.charCodeAt(i);
2903
+ h = Math.imul(h, 16777619) >>> 0;
2904
+ }
2905
+ return h >>> 0;
2906
+ }
2907
+ function atomId(text, guard) {
2908
+ const key = `${text.trim().toLowerCase()}|${(guard ?? "").trim().toLowerCase()}`;
2909
+ const a = fnv32(key, 2166136261).toString(16).padStart(8, "0");
2910
+ const b = fnv32(key, 3421674724).toString(16).padStart(8, "0");
2911
+ return a + b;
2912
+ }
2913
+ function condToText(c) {
2914
+ switch (c.t) {
2915
+ case "true":
2916
+ return null;
2917
+ case "false":
2918
+ return "never";
2919
+ case "atom":
2920
+ return c.text.trim();
2921
+ case "not": {
2922
+ const inner = condToText(c.f);
2923
+ return inner === null ? "never" : `not ${inner}`;
2924
+ }
2925
+ case "and":
2926
+ return c.fs.map((f) => condToText(f) ?? "").filter((x) => x !== "").join(" and ");
2927
+ case "or":
2928
+ return c.fs.map((f) => condToText(f) ?? "").filter((x) => x !== "").join(" or ");
2929
+ }
2930
+ }
2931
+ var FORCE = {
2932
+ obligate: "must",
2933
+ prohibit: "must not",
2934
+ recommend: "should",
2935
+ discourage: "should not",
2936
+ permit: "may"
2937
+ };
2938
+ function directiveAtom(d) {
2939
+ const guard = condToText(d.cond) ?? void 0;
2940
+ const subj = d.subject === "" ? "you" : d.subject;
2941
+ const action = d.action.text.replace(/[.!?\s]+$/, "");
2942
+ const base = `${subj} ${FORCE[d.deontic]} ${action}`;
2943
+ const text = guard ? `When ${guard}, ${base}.` : `${base}.`;
2944
+ return {
2945
+ id: atomId(text, guard),
2946
+ line: d.line,
2947
+ span: d.span,
2948
+ text,
2949
+ kind: "directive",
2950
+ deontic: d.deontic,
2951
+ subject: d.subject,
2952
+ guard,
2953
+ predicates: d.predicate ? [d.predicate] : void 0
2954
+ };
2955
+ }
2956
+ function declarationAtom(d) {
2957
+ const value = d.value.replace(/[.!?\s]+$/, "");
2958
+ const text = d.negated ? `${d.name} is not ${value}.` : `${d.name} is ${value}.`;
2959
+ return {
2960
+ id: atomId(text, void 0),
2961
+ line: d.line,
2962
+ span: d.span,
2963
+ text,
2964
+ kind: "declaration",
2965
+ subject: d.name.toLowerCase()
2966
+ };
2967
+ }
2968
+ function decompose(source) {
2969
+ const rawLines = source.replace(/\r\n/g, "\n").split("\n");
2970
+ const render = renderLines(rawLines);
2971
+ const doc = buildStructure(rawLines);
2972
+ const tools = doc.index.tools;
2973
+ const maskedSource = maskInlineSwitches(source);
2974
+ const maskedLines = maskedSource.replace(/\r\n/g, "\n").split("\n");
2975
+ const conditional = /* @__PURE__ */ new Set();
2976
+ for (const b of parseSwitches(rawLines)) for (let l = b.headerLine; l < b.blockEnd; l += 1) conditional.add(l);
2977
+ for (const s of doc.sections) if (s.guard) for (let l = s.startLine; l < s.endLine; l += 1) conditional.add(l);
2978
+ const cfgMask = configSectionMask(rawLines);
2979
+ const toolMask = toolBlockMask(rawLines);
2980
+ const byLine = /* @__PURE__ */ new Map();
2981
+ for (const node of lower(maskedSource, { tools }).nodes) {
2982
+ const list = byLine.get(node.line);
2983
+ if (list) list.push(node);
2984
+ else byLine.set(node.line, [node]);
2985
+ }
2986
+ const out = [];
2987
+ rawLines.forEach((raw, i) => {
2988
+ const r = render[i];
2989
+ if (!r || r.kind !== "content" || r.lineClass === "md-code" || r.lineClass === "md-heading") return;
2990
+ if (doc.importLines.has(i) || doc.configLines.has(i) || doc.defineLines.has(i) || conditional.has(i) || toolMask[i]) return;
2991
+ if (doc.testLines.has(i) || cfgMask[i]) return;
2992
+ const nodes = byLine.get(i) ?? [];
2993
+ if (nodes.length > 0) {
2994
+ for (const n of nodes) out.push(n.kind === "directive" ? directiveAtom(n) : declarationAtom(n));
2995
+ return;
2996
+ }
2997
+ const { clause } = parseLine(stripInlineComment(maskedLines[i] ?? raw));
2998
+ const lead = raw.length - raw.trimStart().length;
2999
+ for (const sentence of splitSentences(clause)) {
3000
+ for (const seg of sentence.text.split(";")) {
3001
+ const text = seg.trim().replace(/[,]+$/, "");
3002
+ if (text === "") continue;
3003
+ const claim = /[.!?]$/.test(text) ? text : `${text}.`;
3004
+ const predicates = predicatesFromProse(claim, tools);
3005
+ out.push({
3006
+ id: atomId(claim, void 0),
3007
+ line: i,
3008
+ span: { start: lead, end: raw.length },
3009
+ text: claim,
3010
+ kind: "prose",
3011
+ predicates: predicates.length > 0 ? predicates : void 0
3012
+ });
3013
+ }
3014
+ }
3015
+ });
3016
+ return out;
3017
+ }
3018
+
3019
+ // src/core/semantic/config.ts
3020
+ var NLI_MODEL_VERSION = "Xenova/nli-deberta-v3-base@q8";
3021
+ var THETA_EQUIV = 0.8;
3022
+ var THETA_COVER = 0.85;
3023
+ var THETA_CONTRA = 0.97;
3024
+ var THETA_COVERED = 0.85;
3025
+ var THETA_DIRECTION_ACTIONABLE = 0.7;
3026
+ var NLI_DIRECTION_CAP = 0.5;
3027
+ var JACCARD_GATE = 0.1;
3028
+ var MAX_PAIRS = 4e3;
3029
+ var THETA_REDUNDANCY_WARN = 0.1;
3030
+ var THETA_EQUIVALENT_WARN = 0.95;
3031
+
3032
+ // src/core/semantic/candidates.ts
3033
+ var STOP2 = /* @__PURE__ */ new Set([
3034
+ "the",
3035
+ "and",
3036
+ "for",
3037
+ "you",
3038
+ "your",
3039
+ "that",
3040
+ "this",
3041
+ "with",
3042
+ "are",
3043
+ "was",
3044
+ "will",
3045
+ "can",
3046
+ "when",
3047
+ "should",
3048
+ "must",
3049
+ "not",
3050
+ "but",
3051
+ "its",
3052
+ "our",
3053
+ "their",
3054
+ "they",
3055
+ "them",
3056
+ "from",
3057
+ "have",
3058
+ "has",
3059
+ "had",
3060
+ "any",
3061
+ "all",
3062
+ "each",
3063
+ "every",
3064
+ "then",
3065
+ "than",
3066
+ "into",
3067
+ "out",
3068
+ "only",
3069
+ "also",
3070
+ "more",
3071
+ "some",
3072
+ "such",
3073
+ "about",
3074
+ "which",
3075
+ "what",
3076
+ "who",
3077
+ "how",
3078
+ "why",
3079
+ "where",
3080
+ "here",
3081
+ "there",
3082
+ "these",
3083
+ "those",
3084
+ "never",
3085
+ "always",
3086
+ "may",
3087
+ "one",
3088
+ "per",
3089
+ "via",
3090
+ "after",
3091
+ "before",
3092
+ "once"
3093
+ ]);
3094
+ function contentWords(text) {
3095
+ const out = /* @__PURE__ */ new Set();
3096
+ for (const w of text.toLowerCase().match(/[a-z0-9_][a-z0-9_'-]{2,}/g) ?? []) {
3097
+ if (!STOP2.has(w)) out.add(w);
3098
+ }
3099
+ return out;
3100
+ }
3101
+ function jaccard(a, b) {
3102
+ if (a.size === 0 || b.size === 0) return 0;
3103
+ let inter = 0;
3104
+ for (const w of a) if (b.has(w)) inter += 1;
3105
+ return inter / (a.size + b.size - inter);
3106
+ }
3107
+ function predicateGate(a, b) {
3108
+ for (const pa of a.predicates ?? []) {
3109
+ for (const pb of b.predicates ?? []) {
3110
+ if (pa.tool !== void 0 && pa.tool === pb.tool) return true;
3111
+ if (pa.frame !== void 0 && pa.frame === pb.frame) return true;
3112
+ if (isAntonym(pa, pb)) return true;
3113
+ if (pa.head === pb.head) return true;
3114
+ }
3115
+ }
3116
+ return false;
3117
+ }
3118
+ function candidatePairs(atoms) {
3119
+ const words = atoms.map((a) => contentWords(a.text));
3120
+ const pairs = [];
3121
+ for (let i = 0; i < atoms.length; i += 1) {
3122
+ for (let j = i + 1; j < atoms.length; j += 1) {
3123
+ const a = atoms[i];
3124
+ const b = atoms[j];
3125
+ if (!a || !b || a.line === b.line) continue;
3126
+ const wa = words[i];
3127
+ const wb = words[j];
3128
+ if (!wa || !wb) continue;
3129
+ const score = jaccard(wa, wb);
3130
+ const lexical = score >= JACCARD_GATE;
3131
+ const predicate = predicateGate(a, b);
3132
+ if (!lexical && !predicate) continue;
3133
+ pairs.push({ a, b, jaccard: score, via: lexical && predicate ? "both" : lexical ? "lexical" : "predicate" });
3134
+ }
3135
+ }
3136
+ pairs.sort((x, y) => y.jaccard + (y.via !== "lexical" ? 0.1 : 0) - (x.jaccard + (x.via !== "lexical" ? 0.1 : 0)));
3137
+ const truncated = Math.max(0, pairs.length - MAX_PAIRS);
3138
+ return { pairs: pairs.slice(0, MAX_PAIRS), truncated };
3139
+ }
3140
+
3141
+ // src/core/semantic/classify.ts
3142
+ function classifyPair(s) {
3143
+ const entMax = Math.max(s.entAB, s.entBA);
3144
+ if (Math.min(s.conAB, s.conBA) >= THETA_CONTRA && entMax < THETA_EQUIV) return { relation: "contradicts", directionConfidence: 0 };
3145
+ if (s.entAB >= THETA_EQUIV && s.entBA >= THETA_EQUIV) return { relation: "equivalent", directionConfidence: 0 };
3146
+ if (s.entAB >= THETA_COVER && s.entBA < THETA_COVER) {
3147
+ return { relation: "covers", directionConfidence: Math.min(NLI_DIRECTION_CAP, s.entAB - s.entBA) };
3148
+ }
3149
+ if (s.entBA >= THETA_COVER && s.entAB < THETA_COVER) {
3150
+ return { relation: "covered_by", directionConfidence: Math.min(NLI_DIRECTION_CAP, s.entBA - s.entAB) };
3151
+ }
3152
+ return { relation: "distinct", directionConfidence: 0 };
3153
+ }
3154
+ function verdictFrom(a, b, s, tier = "nli") {
3155
+ const { relation, directionConfidence } = classifyPair(s);
3156
+ return {
3157
+ a,
3158
+ b,
3159
+ relation,
3160
+ entAB: s.entAB,
3161
+ entBA: s.entBA,
3162
+ conMax: Math.max(s.conAB, s.conBA),
3163
+ conMin: Math.min(s.conAB, s.conBA),
3164
+ directionConfidence,
3165
+ tier,
3166
+ modelVersion: NLI_MODEL_VERSION
3167
+ };
3168
+ }
3169
+ function assembleAudits(atoms, verdicts, coverage) {
3170
+ const atomById = new Map(atoms.map((a) => [a.id, a]));
3171
+ const covByAtom = new Map(coverage.map((c) => [c.atom, c]));
3172
+ const byLine = /* @__PURE__ */ new Map();
3173
+ for (const a of atoms) {
3174
+ const list = byLine.get(a.line);
3175
+ if (list) list.push(a);
3176
+ else byLine.set(a.line, [a]);
3177
+ }
3178
+ const audits = [];
3179
+ for (const [line, lineAtoms] of [...byLine.entries()].sort((x, y) => x[0] - y[0])) {
3180
+ const coverers = /* @__PURE__ */ new Map();
3181
+ let minCoverage = 1;
3182
+ let allCovered = true;
3183
+ for (const atom of lineAtoms) {
3184
+ const cov = covByAtom.get(atom.id);
3185
+ const score = cov?.score ?? 0;
3186
+ minCoverage = Math.min(minCoverage, score);
3187
+ if (score < THETA_COVERED) allCovered = false;
3188
+ if (!cov) continue;
3189
+ for (const covererId of cov.coveredBy) {
3190
+ const coverer = atomById.get(covererId);
3191
+ if (!coverer || coverer.line === line) continue;
3192
+ const rec = coverers.get(coverer.line) ?? { atoms: /* @__PURE__ */ new Set(), ent: 1 };
3193
+ rec.atoms.add(covererId);
3194
+ rec.ent = Math.min(rec.ent, score);
3195
+ coverers.set(coverer.line, rec);
3196
+ }
3197
+ }
3198
+ const conflicts = /* @__PURE__ */ new Map();
3199
+ for (const v of verdicts) {
3200
+ if (v.relation !== "contradicts") continue;
3201
+ const la = atomById.get(v.a)?.line;
3202
+ const lb = atomById.get(v.b)?.line;
3203
+ const other = la === line ? lb : lb === line ? la : void 0;
3204
+ if (other === void 0 || other === line) continue;
3205
+ const prev = conflicts.get(other);
3206
+ conflicts.set(other, {
3207
+ conf: Math.max(prev?.conf ?? 0, v.conMin),
3208
+ confirmed: (prev?.confirmed ?? false) || v.tier === "nli+judge"
3209
+ });
3210
+ }
3211
+ audits.push({
3212
+ line,
3213
+ atoms: lineAtoms.map((a) => a.id),
3214
+ residual: 1 - minCoverage,
3215
+ coveredBy: [...coverers.entries()].map(([l, rec]) => ({ line: l, atoms: [...rec.atoms], ent: rec.ent })).sort((x, y) => y.ent - x.ent),
3216
+ contradicts: [...conflicts.entries()].map(([l, rec]) => ({ line: l, conf: rec.conf, confirmed: rec.confirmed })).sort((x, y) => y.conf - x.conf),
3217
+ proposedRemovable: lineAtoms.length > 0 && allCovered
3218
+ });
3219
+ }
3220
+ return audits;
3221
+ }
3222
+ function buildReport(atoms, verdicts, coverage, stats) {
3223
+ return { atoms, pairs: verdicts, coverage, lines: assembleAudits(atoms, verdicts, coverage), stats };
3224
+ }
3225
+
3226
+ // src/core/semantic/reduce.ts
3227
+ async function proposeReduction(source, analyze, opts = {}) {
3228
+ const maxCuts = Math.max(1, opts.maxCuts ?? 25);
3229
+ let lines = source.replace(/\r\n/g, "\n").split("\n");
3230
+ let origin = lines.map((_, i) => i);
3231
+ const steps = [];
3232
+ while (steps.length < maxCuts) {
3233
+ const report2 = await analyze(lines.join("\n"));
3234
+ const removable = report2.lines.filter((l) => l.proposedRemovable).sort((a, b) => a.residual - b.residual);
3235
+ const target = removable[0];
3236
+ if (!target) break;
3237
+ steps.push({
3238
+ order: steps.length + 1,
3239
+ line: origin[target.line] ?? target.line,
3240
+ text: (lines[target.line] ?? "").trim(),
3241
+ residualAtCut: target.residual,
3242
+ coveredBy: target.coveredBy.map((c) => ({ line: origin[c.line] ?? c.line, ent: c.ent }))
3243
+ });
3244
+ lines = lines.filter((_, i) => i !== target.line);
3245
+ origin = origin.filter((_, i) => i !== target.line);
3246
+ }
3247
+ return steps;
3248
+ }
3249
+
3250
+ // src/core/semantic/report.ts
3251
+ function lineRange(rawLines, line) {
3252
+ const raw = rawLines[line] ?? "";
3253
+ return { start: raw.length - raw.trimStart().length, end: raw.length };
3254
+ }
3255
+ var fmtLines = (ls, disp = (i) => i + 1) => `line${ls.length > 1 ? "s" : ""} ${ls.map((l) => disp(l)).join(", ")}`;
3256
+ function semanticDiagnostics(report2, source, displaySource = source) {
3257
+ const rawLines = source.replace(/\r\n/g, "\n").split("\n");
3258
+ const ordinals = lineOrdinals(displaySource.replace(/\r\n/g, "\n").split("\n"));
3259
+ const disp = (i) => displayLine(ordinals, i);
3260
+ const atomById = new Map(report2.atoms.map((a) => [a.id, a]));
3261
+ const out = [];
3262
+ const equivalentWith = /* @__PURE__ */ new Map();
3263
+ for (const v of report2.pairs) {
3264
+ if (v.relation !== "equivalent") continue;
3265
+ const la = atomById.get(v.a)?.line;
3266
+ const lb = atomById.get(v.b)?.line;
3267
+ if (la === void 0 || lb === void 0 || la === lb) continue;
3268
+ const strength = Math.min(v.entAB, v.entBA);
3269
+ for (const [self, other] of [[la, lb], [lb, la]]) {
3270
+ const map = equivalentWith.get(self) ?? /* @__PURE__ */ new Map();
3271
+ map.set(other, Math.max(map.get(other) ?? 0, strength));
3272
+ equivalentWith.set(self, map);
3273
+ }
3274
+ }
3275
+ for (const audit of report2.lines) {
3276
+ const { start, end } = lineRange(rawLines, audit.line);
3277
+ const push = (sourceName, severity, message) => {
3278
+ out.push({ source: sourceName, severity, line: audit.line, start, end, message });
3279
+ };
3280
+ const conflict = audit.contradicts.find((c) => c.confirmed);
3281
+ if (conflict) {
3282
+ push(
3283
+ "semantic/contradicts",
3284
+ "warn",
3285
+ `Semantic conflict \u2014 this appears to contradict line ${disp(conflict.line)} (confidence ${conflict.conf.toFixed(2)}). Advisory: review both lines.`
3286
+ );
3287
+ continue;
3288
+ }
3289
+ if (audit.proposedRemovable && audit.coveredBy.length > 0) {
3290
+ const coverLines = audit.coveredBy.map((c) => c.line);
3291
+ const minEnt = Math.min(...audit.coveredBy.map((c) => c.ent));
3292
+ const high = audit.residual <= THETA_REDUNDANCY_WARN;
3293
+ push(
3294
+ "semantic/covered",
3295
+ high ? "warn" : "info",
3296
+ `${high ? "High redundancy" : "Covered"} \u2014 everything this line asserts is already said by ${fmtLines(coverLines, disp)} (the semantic tier reads entailment \u2265 ${minEnt.toFixed(2)}), leaving only ${Math.round(audit.residual * 100)}% of its meaning as its own (residual ${audit.residual.toFixed(2)}). Consolidate into one statement \u2014 and verify with the test suite before cutting.`
3297
+ );
3298
+ continue;
3299
+ }
3300
+ const dupes = equivalentWith.get(audit.line);
3301
+ if (dupes && dupes.size > 0) {
3302
+ const strongest = Math.max(...dupes.values());
3303
+ const high = strongest >= THETA_EQUIVALENT_WARN;
3304
+ push(
3305
+ "semantic/equivalent",
3306
+ high ? "warn" : "info",
3307
+ `${high ? "High redundancy \u2014 interchangeable with" : "Duplicate meaning \u2014 says the same thing as"} ${fmtLines([...dupes.keys()], disp)} (each implies the other, mutual entailment \u2265 ${strongest.toFixed(2)}). State it once and reference it.`
3308
+ );
3309
+ }
3310
+ }
3311
+ for (const v of report2.pairs) {
3312
+ if (v.relation !== "covers" && v.relation !== "covered_by" || v.directionConfidence < THETA_DIRECTION_ACTIONABLE) continue;
3313
+ const narrowAtom = v.relation === "covers" ? v.b : v.a;
3314
+ const broadAtom = v.relation === "covers" ? v.a : v.b;
3315
+ const narrow = atomById.get(narrowAtom);
3316
+ const broad = atomById.get(broadAtom);
3317
+ if (!narrow || !broad || narrow.line === broad.line) continue;
3318
+ if (out.some((d) => d.line === narrow.line)) continue;
3319
+ const { start, end } = lineRange(rawLines, narrow.line);
3320
+ out.push({
3321
+ source: "semantic/covered",
3322
+ severity: "info",
3323
+ line: narrow.line,
3324
+ start,
3325
+ end,
3326
+ message: `Subsumed \u2014 a broader rule on line ${disp(broad.line)} already implies this (confidence ${v.directionConfidence.toFixed(2)}). Consider removing the narrower restatement.`
3327
+ });
3328
+ }
3329
+ return out;
3330
+ }
3331
+
3332
+ // src/core/resolve.ts
3333
+ var norm = (v) => String(v ?? "").trim().toLowerCase();
3334
+ function resolve(source, bag, opts = {}) {
3335
+ const lines = source.split("\n");
3336
+ return applyResolve(lines, buildStructure(lines), parseSwitches(lines), bag, opts);
3337
+ }
3338
+ function applyResolve(lines, doc, switches, bag, opts = {}) {
3339
+ const fullBag = { ...bag };
3340
+ const defs = [];
3341
+ for (const [name, d] of doc.defines) {
3342
+ const ast = parseExpr(d.expr).ast;
3343
+ if (ast) defs.push({ name, ast });
3344
+ }
3345
+ for (let pass = 0; pass < defs.length; pass += 1) for (const { name, ast } of defs) fullBag[name] = evalExpr(ast, fullBag);
3346
+ const rewrite = /* @__PURE__ */ new Map();
3347
+ const blockVars = (block) => {
3348
+ if (block.kind === "value") return [block.varName];
3349
+ if (block.kind === "tuple") return block.varNames ?? [];
3350
+ const names = /* @__PURE__ */ new Set();
3351
+ for (const arm of block.arms) {
3352
+ if (arm.predicate === void 0) continue;
3353
+ const { ast } = parseExpr(arm.predicate);
3354
+ if (ast) for (const a of exprAtoms(ast)) names.add(a.name);
3355
+ }
3356
+ return [...names];
3357
+ };
3358
+ const allBound = (vars) => vars.every((v) => v in fullBag);
3359
+ for (const block of switches) {
3360
+ if (block.error !== null) continue;
3361
+ if (!allBound(blockVars(block))) continue;
3362
+ for (let l = block.headerLine; l < block.blockEnd; l += 1) rewrite.set(l, null);
3363
+ let p = block.headerLine - 1;
3364
+ while (p >= 0 && ((lines[p] ?? "").trim() === "" || (lines[p] ?? "").trim().startsWith("//"))) p -= 1;
3365
+ if (p >= 0 && isAnnotationLine(lines[p] ?? "")) rewrite.set(p, null);
3366
+ const arm = block.kind === "predicate" ? selectPredicateArm(block, (expr) => {
3367
+ const { ast } = parseExpr(expr);
3368
+ return ast === null ? false : evalExpr(ast, fullBag);
3369
+ }) : block.kind === "tuple" ? selectTupleArm(block, (block.varNames ?? []).map((n) => norm(fullBag[n]))) : selectArm(block, norm(fullBag[block.varName]));
3370
+ if (arm === null) continue;
3371
+ if (arm.inlineBody !== "") rewrite.set(arm.headerLine, " ".repeat(block.indent) + arm.inlineBody);
3372
+ const lead = (s) => s.length - s.trimStart().length;
3373
+ const bodyLines = [];
3374
+ for (let l = arm.headerLine + 1; l < arm.spanEnd; l += 1) bodyLines.push(l);
3375
+ const nonBlank = bodyLines.filter((l) => (lines[l] ?? "").trim() !== "");
3376
+ if (nonBlank.length === 0) continue;
3377
+ const base = Math.min(...nonBlank.map((l) => lead(lines[l] ?? "")));
3378
+ for (const l of bodyLines) {
3379
+ const raw = lines[l] ?? "";
3380
+ if (raw.trim() === "") {
3381
+ rewrite.set(l, "");
3382
+ continue;
3383
+ }
3384
+ rewrite.set(l, " ".repeat(block.indent + Math.max(0, lead(raw) - base)) + raw.trimStart());
3385
+ }
3386
+ }
3387
+ const stripWhen = (line) => (lines[line] ?? "").replace(/\s*\bwhen=[^>]*>/i, ">");
3388
+ for (const sec of doc.sections) {
3389
+ if (sec.syntax !== "xml" || !sec.guard) continue;
3390
+ const { ast } = parseExpr(sec.guard.text);
3391
+ const guardVars = ast ? exprAtoms(ast).map((a) => a.name) : [];
3392
+ if (!allBound(guardVars)) {
3393
+ if (opts.keepUndecidedGuards !== true) rewrite.set(sec.startLine, stripWhen(sec.startLine));
3394
+ continue;
3395
+ }
3396
+ const keep = ast === null ? true : evalExpr(ast, fullBag);
3397
+ if (keep) rewrite.set(sec.startLine, stripWhen(sec.startLine));
3398
+ else for (let l = sec.startLine; l < sec.endLine; l += 1) rewrite.set(l, null);
3399
+ }
3400
+ const pick = (v) => norm(fullBag[v]);
3401
+ const out = [];
3402
+ lines.forEach((line, i) => {
3403
+ if (!rewrite.has(i)) {
3404
+ out.push(resolveInlineText(line, pick));
3405
+ return;
3406
+ }
3407
+ const r = rewrite.get(i);
3408
+ if (r !== null && r !== void 0) out.push(resolveInlineText(r, pick));
3409
+ });
3410
+ return out.join("\n");
3411
+ }
3412
+
3413
+ // src/core/compile.ts
3414
+ function resolveModel(blocks, services) {
3415
+ const m = blocks.find((b) => b.kind === "model");
3416
+ if (!m || m.value === "") return void 0;
3417
+ const num = (k) => {
3418
+ const v = m.params.get(k);
3419
+ const n = v === void 0 ? NaN : Number(v);
3420
+ return Number.isFinite(n) ? n : void 0;
3421
+ };
3422
+ const via = m.params.get("via");
3423
+ const svc = via !== void 0 && via !== "" ? services.find((s) => s.name === via) : void 0;
3424
+ return {
3425
+ id: m.value,
3426
+ maxTokens: num("max_tokens"),
3427
+ temperature: num("temperature"),
3428
+ ...svc ? { service: { name: svc.name, base: svc.base, headers: svc.headers } } : {}
3429
+ };
3430
+ }
3431
+ function compile(source, opts = {}) {
3432
+ const rawLines = source.split("\n");
3433
+ const diagnostics = runDiagnostics(rawLines, opts.rubric ?? [], { isWord: opts.isWord, addressees: opts.addressees, glishConfig: opts.glishConfig });
3434
+ const doc = buildStructure(rawLines);
3435
+ const model = resolveModel(parseConfigBlocks(rawLines), parseServices(rawLines).services);
3436
+ return {
3437
+ ir: lower(source),
3438
+ diagnostics,
3439
+ agent: { config: Object.fromEntries(doc.config), model, tools: doc.toolDefs },
3440
+ consistent: !diagnostics.some((d) => d.source.startsWith("logic/") && d.severity === "error"),
3441
+ wellFormed: !diagnostics.some((d) => d.source.startsWith("structure/") && d.severity === "error"),
3442
+ // Step anchors (`#greet`) are author-facing identity — resolve them to step numbers and strip declarations
3443
+ // before tagging, so the compiled prompt the model runs is clean and its step references are followable.
3444
+ xml: compileToXml(resolveAnchors(source), resolveAnchors(opts.instructions ?? ""))
3445
+ };
3446
+ }
3447
+ function numericDiagnostics(conflicts, source) {
3448
+ const lines = source.split("\n");
3449
+ const ordinals = lineOrdinals(lines);
3450
+ const disp = (i) => displayLine(ordinals, i);
3451
+ const out = [];
3452
+ for (const c of conflicts) {
3453
+ for (const ln of c.lines) {
3454
+ const raw = lines[ln] ?? "";
3455
+ const lead = raw.length - raw.trimStart().length;
3456
+ const other = c.lines.find((x) => x !== ln);
3457
+ out.push({
3458
+ source: "logic/numeric",
3459
+ severity: "error",
3460
+ line: ln,
3461
+ start: lead,
3462
+ end: raw.length,
3463
+ message: other !== void 0 ? `${c.message} (conflicts with line ${disp(other)})` : c.message
3464
+ });
3465
+ }
3466
+ }
3467
+ return out;
3468
+ }
3469
+ function structuralDiagnostics(conflicts, source) {
3470
+ const lines = source.split("\n");
3471
+ const ordinals = lineOrdinals(lines);
3472
+ const disp = (i) => displayLine(ordinals, i);
3473
+ const out = [];
3474
+ for (const c of conflicts) {
3475
+ for (const ln of c.lines) {
3476
+ const raw = lines[ln] ?? "";
3477
+ const lead = raw.length - raw.trimStart().length;
3478
+ const other = c.lines.find((x) => x !== ln);
3479
+ out.push({
3480
+ source: `structure/${c.code}`,
3481
+ severity: c.severity,
3482
+ line: ln,
3483
+ start: lead,
3484
+ end: raw.length,
3485
+ message: other !== void 0 ? `${c.message} (conflicts with line ${disp(other)})` : c.message
3486
+ });
3487
+ }
3488
+ }
3489
+ return out;
3490
+ }
3491
+ function prepareTemplate(source) {
3492
+ const lines = source.split("\n");
3493
+ const doc = buildStructure(lines);
3494
+ const switches = parseSwitches(lines);
3495
+ return {
3496
+ source,
3497
+ resolve: (bag) => applyResolve(lines, doc, switches, bag),
3498
+ render: (bag) => compileToXml(applyResolve(lines, doc, switches, bag), "")
3499
+ };
3500
+ }
3501
+
3502
+ // src/core/report.ts
3503
+ var SEV_RANK2 = { error: 3, warn: 2, info: 1 };
3504
+ function splitCode(source) {
3505
+ const i = source.indexOf("/");
3506
+ return i < 0 ? { category: source, name: source } : { category: source.slice(0, i), name: source.slice(i + 1) };
3507
+ }
3508
+ function effectiveSeverity(d, category, importance, policy) {
3509
+ const listed = (list) => !!list && (list.includes(d.source) || list.includes(category));
3510
+ if (listed(policy.relax)) return "info";
3511
+ if (d.severity === "error") return "error";
3512
+ const raise = listed(policy.escalate) || policy.escalateImportance !== void 0 && atLeastImportant(importance, policy.escalateImportance) || policy.strict === true && d.severity === "warn";
3513
+ return raise ? "error" : d.severity;
3514
+ }
3515
+ function describeType(type) {
3516
+ if (!type) return "untyped";
3517
+ return type.kind === "enum" ? `enum(${(type.members ?? []).join("|")})` : type.kind;
3518
+ }
3519
+ function summarize(source) {
3520
+ const lines = source.split("\n");
3521
+ const doc = buildStructure(lines);
3522
+ const blocks = parseSwitches(lines);
3523
+ const count = (kind) => blocks.filter((b) => b.kind === kind && b.error === null).length;
3524
+ return {
3525
+ inputs: doc.imports.filter((im) => im.kind === "variable").map((im) => ({ name: im.name, type: describeType(doc.inputTypes.get(im.name)) })),
3526
+ defines: [...doc.defines.keys()],
3527
+ sections: doc.sections.map((s) => s.tag),
3528
+ tools: [...doc.index.tools],
3529
+ routes: [...doc.index.routes],
3530
+ steps: doc.steps.length,
3531
+ switches: { value: count("value"), predicate: count("predicate"), tuple: count("tuple"), inline: parseInlineSwitches(lines).filter((s) => s.error === null).length }
3532
+ };
3533
+ }
3534
+ function report(source, opts = {}) {
3535
+ const policy = opts.policy ?? {};
3536
+ const compiled = compile(source, opts.compile);
3537
+ const all = [...compiled.diagnostics, ...opts.extraDiagnostics ?? []];
3538
+ const diagnostics = all.map((d) => {
3539
+ const { category, name } = splitCode(d.source);
3540
+ const importance = importanceOf(d.source, d.severity);
3541
+ const eff = effectiveSeverity(d, category, importance, policy);
3542
+ return {
3543
+ code: d.source,
3544
+ category,
3545
+ name,
3546
+ importance,
3547
+ severity: d.severity,
3548
+ effectiveSeverity: eff,
3549
+ blocking: eff === "error",
3550
+ range: { line: d.line + 1, column: d.start + 1, endColumn: d.end + 1 },
3551
+ message: d.message,
3552
+ ...d.fix ? { fix: d.fix } : {}
3553
+ };
3554
+ }).sort((a, b) => a.range.line - b.range.line || SEV_RANK2[b.effectiveSeverity] - SEV_RANK2[a.effectiveSeverity]);
3555
+ const counts = {
3556
+ error: diagnostics.filter((d) => d.effectiveSeverity === "error").length,
3557
+ warn: diagnostics.filter((d) => d.effectiveSeverity === "warn").length,
3558
+ info: diagnostics.filter((d) => d.effectiveSeverity === "info").length
3559
+ };
3560
+ return {
3561
+ ok: counts.error === 0,
3562
+ consistent: compiled.consistent,
3563
+ wellFormed: compiled.wellFormed,
3564
+ counts,
3565
+ diagnostics,
3566
+ program: summarize(source),
3567
+ xml: compiled.xml
3568
+ };
3569
+ }
3570
+
3571
+ export { CHARSET_DOC, INDENT_UNIT, OBLIGATE, PROHIBIT, applyFix, assembleAudits, atLeastImportant, buildReport, candidatePairs, classifyPair, compile, composeSpans, decompose, importanceOf, layoutColumns, numericDiagnostics, prepareTemplate, proposeReduction, rename, renderLines, report, resolve, ruleCoverage, runDiagnostics, semanticDiagnostics, structuralDiagnostics, verdictFrom };
3572
+ //# sourceMappingURL=chunk-KRYC4OTL.js.map
3573
+ //# sourceMappingURL=chunk-KRYC4OTL.js.map