@polycode-projects/the-mechanical-code-talker 1.9.1 → 1.10.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 (81) hide show
  1. package/README.md +441 -217
  2. package/bin/tmct.mjs +126 -1
  3. package/corpus/seon/README.md +1 -2
  4. package/package.json +4 -2
  5. package/src/answer-variants.mjs +8 -36
  6. package/src/ask-browser-entry.mjs +5 -23
  7. package/src/ask-browser.bundle.js +1 -2
  8. package/src/ask-nlp.mjs +9 -23
  9. package/src/ask-vocab.mjs +139 -589
  10. package/src/ask.mjs +627 -1729
  11. package/src/chat.mjs +1684 -2874
  12. package/src/cli-args.mjs +14 -28
  13. package/src/codegraph.mjs +236 -644
  14. package/src/completions/complete.mjs +18 -62
  15. package/src/completions/graph-adapter.mjs +14 -60
  16. package/src/completions/group.mjs +12 -68
  17. package/src/completions/infer.mjs +38 -126
  18. package/src/completions/prune.mjs +17 -70
  19. package/src/completions/rank.mjs +16 -69
  20. package/src/completions/search.mjs +8 -31
  21. package/src/concept.mjs +32 -88
  22. package/src/conformance.mjs +11 -15
  23. package/src/corpus/conceptnet.mjs +31 -89
  24. package/src/corpus/templates.mjs +19 -45
  25. package/src/corpus/unknown-ingest.mjs +31 -92
  26. package/src/embed.mjs +10 -22
  27. package/src/extensions.mjs +50 -154
  28. package/src/finish.mjs +35 -91
  29. package/src/grammar/ace.mjs +16 -40
  30. package/src/grammar/assert.mjs +1 -1
  31. package/src/grammar/lexicon-core.json +1 -1
  32. package/src/grammar/lexicon.mjs +9 -27
  33. package/src/graph-merge.mjs +2 -3
  34. package/src/hash.mjs +6 -14
  35. package/src/index.mjs +6 -10
  36. package/src/init.mjs +38 -125
  37. package/src/interpret/fuzzy.mjs +10 -29
  38. package/src/interpret/merge.mjs +9 -27
  39. package/src/interpret/normalize.mjs +137 -585
  40. package/src/interpret/pipeline.mjs +23 -71
  41. package/src/interpret/strategies/ace.mjs +7 -31
  42. package/src/interpret/strategies/constructions.mjs +14 -41
  43. package/src/interpret/strategies/grammar.mjs +21 -60
  44. package/src/interpret/strategies/keywords.mjs +42 -131
  45. package/src/interpret/strategies/noise-strip.mjs +18 -89
  46. package/src/memory/bias.mjs +11 -54
  47. package/src/memory/blocks.mjs +18 -69
  48. package/src/memory/core.mjs +171 -591
  49. package/src/memory/fold.mjs +0 -0
  50. package/src/memory/inspect.mjs +7 -25
  51. package/src/memory/shacl.mjs +10 -39
  52. package/src/memory/trust.mjs +26 -127
  53. package/src/memory-ask-browser-entry.mjs +7 -30
  54. package/src/memory-ask-browser.bundle.js +1 -1
  55. package/src/paraphrase.mjs +20 -53
  56. package/src/planning.mjs +15 -157
  57. package/src/prose-nlp.mjs +4 -17
  58. package/src/prose.mjs +19 -67
  59. package/src/providers/bootstrap.mjs +1 -2
  60. package/src/providers/fixture.mjs +1 -2
  61. package/src/providers/graph-service.mjs +28 -59
  62. package/src/repository-interface.mjs +6 -8
  63. package/src/router/drive.mjs +183 -0
  64. package/src/router/goal-reasoner.mjs +66 -231
  65. package/src/router/guardrail.mjs +20 -58
  66. package/src/router/planner.mjs +15 -46
  67. package/src/router/registry.mjs +13 -43
  68. package/src/router/resolver.mjs +46 -131
  69. package/src/router/results.mjs +231 -0
  70. package/src/schema-docs.mjs +10 -27
  71. package/src/server-http.mjs +10 -19
  72. package/src/server.mjs +22 -28
  73. package/src/sessions.mjs +15 -30
  74. package/src/source-slice.mjs +5 -7
  75. package/src/source.mjs +10 -20
  76. package/src/syllogise.mjs +187 -575
  77. package/src/telemetry.mjs +3 -3
  78. package/src/toml-config.mjs +4 -4
  79. package/src/tui/app.mjs +9 -19
  80. package/src/viz.mjs +66 -123
  81. package/src/wink-model.mjs +10 -24
@@ -1,15 +1,9 @@
1
- // interpret/normalize.mjs — the input-normalization pass (ROADMAP item 10) and the
2
- // shared text-prep helpers every interpretation strategy reads. Extracted MOVE-only
3
- // from ask.mjs (item 13, chat/primitives split): the code here is the §3.5
4
- // normalization pipeline exactly as it ran inside ask.mjs — contractions expanded,
5
- // curated misspelling/wrong-word corrections applied, g-dropped words restored,
6
- // filler/politeness stripped, then the small closed set of rhetorical frames
7
- // rewritten to the canonical form of the SAME question. Pure, deterministic,
8
- // idempotent — both parsing strategies (and any future one) see identical text.
9
- //
10
- // This is the pipeline's documented PRE-PASS: interpret/pipeline.mjs runs
11
- // normalizeInput() once, hands every strategy the normalized text (plus the raw
12
- // text in ctx.raw), and records whether normalization changed the input.
1
+ // interpret/normalize.mjs — the input-normalization pass and shared text-prep
2
+ // helpers every interpretation strategy reads: contractions expanded,
3
+ // curated misspelling/wrong-word corrections applied, g-dropped words
4
+ // restored, filler/politeness stripped, then a small closed set of
5
+ // rhetorical frames rewritten to the canonical form of the same question.
6
+ // Pure, deterministic, idempotent.
13
7
 
14
8
  import {
15
9
  CONTRACTIONS, MISSPELLINGS, WRONG_WORDS, G_DROP, FILLER_WORDS,
@@ -20,7 +14,7 @@ export function escapeRegex(s) {
20
14
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
21
15
  }
22
16
 
23
- // ---- §3.5 normalization — runs before EITHER parsing strategy sees the text ----
17
+ // ---- normalization — runs before EITHER parsing strategy sees the text ----
24
18
 
25
19
  /** contraction/informal-spelling table -> word-boundary regex, longest phrase
26
20
  * first (so "there's" doesn't get shadowed by a shorter overlapping entry). */
@@ -29,14 +23,9 @@ const tableRe = (table) => new RegExp(
29
23
  "gi",
30
24
  );
31
25
  const CONTRACTION_RE = tableRe(CONTRACTIONS);
32
- // misspelling/wrong-word CORRECTIONS (ask-vocab.mjs) same mechanism, applied
33
- // after contractions: restore the intended spelling first, then map misused
34
- // words to their canonical schema term. Deterministic and curated, so they run
35
- // BEFORE either parse strategy and ahead of the bounded edit-distance fallback.
36
- // The trailing lookahead refuses to rewrite a word glued to a dotted extension:
37
- // WRONG_WORDS entries are real English words that plausibly NAME modules
38
- // ("revision.mjs", "property.py"), and a correction that corrupts an object
39
- // term would be a guess — the exact thing these tables exist to avoid.
26
+ // Misspelling/wrong-word corrections (ask-vocab.mjs). The trailing lookahead
27
+ // refuses a word glued to a dotted extension, since WRONG_WORDS entries are
28
+ // real words that can also name a module ("revision.mjs").
40
29
  const correctionRe = (table) => new RegExp(
41
30
  "\\b(" + Object.keys(table).sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b(?!\\.[a-z0-9])",
42
31
  "gi",
@@ -44,117 +33,46 @@ const correctionRe = (table) => new RegExp(
44
33
  const MISSPELLING_RE = correctionRe(MISSPELLINGS);
45
34
  const WRONG_WORD_RE = correctionRe(WRONG_WORDS);
46
35
 
47
- // ---- two deferred typo sub-cases (HANDOVER.md 2026-07-12, routed from
48
- // BENCHMARK_CONVERSATION_1.7.0.md's "Routed backlog": "cochange w/ wat",
49
- // "tests 4 it") that structurally CANNOT live in the MISSPELLINGS/WRONG_WORDS
50
- // tables above, for two independent reasons:
51
- // - correctionRe() builds `\b(...)\b` from a table's keys. A key of "w/"
52
- // can never satisfy the trailing `\b`: both "/" and the whitespace that
53
- // follows it are non-word characters, so no word-boundary transition
54
- // ever happens there — no table entry, however spelled, can match.
55
- // - "with" and "for" are not canonical grammar-owned words (not in
56
- // VERB_TO_KIND/ENTITY_TO_TYPE/MODIFIER_TO_KIND/TRIGGER_WORDS), and
57
- // test/ask-vocab.test.mjs enforces that every correction TABLE value is
58
- // one of those — by design, so a table entry couldn't rewrite INTO a
59
- // non-grammar word either.
60
- // Same species as chat.mjs's VAGUE_TOUCH_TEL_RE/VAGUE_TOUCH_ABUT_RE (a
61
- // dedicated, narrowly-scoped regex pass instead of a table entry), wired
62
- // here rather than in chat.mjs because "w/" and leetspeak "4" are GENERAL
63
- // shorthand a developer can use in any question, not one lane's own anchor
64
- // word. normalize.mjs's normalizeQuery is the single point both ask.mjs's
65
- // parseQuery and interpret/pipeline.mjs's normalizeInput funnel every query
66
- // through, so wiring it here reaches every caller once. ----
67
-
68
- /** "w/" -> "with". Lookbehind/lookahead require whitespace (or start/end of
69
- * string) on BOTH sides, so a real path fragment ("src/w/foo.mjs" — the "/"
70
- * right before "w" is not whitespace) and the DIFFERENT shorthand "w/o"
71
- * ("without" — a non-whitespace "o" right after the slash) never match. */
36
+ // Two typo sub-cases that can't live in the MISSPELLINGS/WRONG_WORDS tables:
37
+ // "w/" has no word boundary for correctionRe() to anchor on, and "with"/"for"
38
+ // aren't grammar-owned vocabulary words.
39
+
40
+ /** "w/" -> "with", not "w/o" ("without") or a path fragment ("src/w/foo.mjs"). */
72
41
  const W_SLASH_RE = /(?<=^|\s)w\/(?=\s|$)/gi;
73
42
 
74
- /** "4" meaning the word "for" deliberately NOT a context-free standalone-
75
- * digit substitution. fuzzy.mjs's eligibleForCanon() and several independent
76
- * `/^[a-z]+$/` guards in ask.mjs protect digit tokens everywhere in this
77
- * codebase (shas, line numbers, counts — "top 4 results", "line 4", "commit
78
- * 4a2b…") on purpose; a blind "4" -> "for" token rule would corrupt every
79
- * one of those. Narrowed to two closed, well-justified trigger shapes —
80
- * chosen SMALLER than the plausible "wait 4 X" / "used 4 X" leetspeak
81
- * because both of those anchors collide with genuine counts in ordinary
82
- * English ("wait 4 minutes", "used 4 times"), which this rule must never
83
- * touch:
84
- * - a GRATITUDE interjection immediately before "4" (the same closed word
85
- * list THANKS_PREAMBLE_RE above already trusts as pure gratitude, never
86
- * a count-report opener) — "thx 4 the help", "cheers 4 that".
87
- * - the "4 example"/"4 instance" idiom, guarded to fire ONLY when nothing
88
- * else follows on the same clause (end of string or punctuation next) —
89
- * "…, 4 example" / "4 example?" is the parenthetical "for example" idiom,
90
- * but "the 4 example modules" names a genuine COUNT of four example
91
- * modules, and the trailing-word lookahead refuses to rewrite that.
92
- */
43
+ /** Leetspeak "4" -> "for", narrowed to two closed trigger shapes (a gratitude
44
+ * interjection before it, or the "4 example/instance" idiom) so it never
45
+ * touches a genuine count ("wait 4 minutes", "commit 4a2b…"). */
93
46
  const FOR_DIGIT_THANKS_RE = /\b(thx|thanks|thank\s+you|many\s+thanks|ty|cheers)\s+4\b/gi;
94
47
  const FOR_DIGIT_EXAMPLE_RE = /\b4\s+(example|instance)\b(?!\s*[a-z])/gi;
95
48
 
96
- /** "that class"/"this module"/"that function" (a context pronoun immediately
97
- * followed by the SINGULAR kind noun it's already standing in for) -> the bare
98
- * pronoun alone (0.9.15 Tier-1 single-touch playtest). "which class contains
99
- * Task.complete" answers by NAMING the class (" there is Task"); a curious
100
- * user's very next turn naturally says "what is in that class", not the bare
101
- * "what is in that" the grammar already understands. Left unstripped, the two
102
- * parse strategies disagreed on the SPAN (grammar kept "that class" as one
103
- * literal 2-word object term; keyword-spot split off "class" as an entityType
104
- * keyword, leaving bare "that" as the object) — same PARSE, different shape,
105
- * so merge.mjs's honest {ambiguousParse} tie fired even though a human reads
106
- * this as one unambiguous sentence. Folding the kind noun away BEFORE either
107
- * strategy runs leaves exactly one reading: CONTEXT_PRONOUNS' own existing
108
- * focus-resolution (ask.mjs's resolveTermOrContext) then takes it from there,
109
- * unchanged — this frame only removes the strategy disagreement, it does not
110
- * touch how the pronoun itself resolves. Singular kind nouns only ("that
111
- * classes" isn't grammatical, so plurals are never a real anaphora and are
112
- * left alone); "one" is excluded ("that one" is already its own literal
113
- * CONTEXT_PRONOUNS entry). */
49
+ /** "that class"/"this module" (context pronoun + the singular kind noun it
50
+ * already stands in for) -> the bare pronoun, so both parse strategies agree
51
+ * on one span instead of disagreeing into a false {ambiguousParse}. Plurals
52
+ * are left alone (never real anaphora); "one" already has its own
53
+ * CONTEXT_PRONOUNS entry. */
114
54
  const KIND_NOUN_ANAPHORA_RE = /\b(this|that)\s+(class|module|function|method|attribute|variable|file|commit)\b/gi;
115
55
 
116
- /** Read-only PROBE (BENCHMARK_CONVERSATION_1.7.0.md routed backlog C2): does
117
- * `text` contain a KIND_NOUN_ANAPHORA_RE match, and if so, what ENTITY CLASS
118
- * does its kind noun name (ENTITY_TO_TYPE, ask-vocab.mjs the same
119
- * "file"->"Module" convention every other lane in this grammar already
120
- * uses)? Returns that class, or null when no such anaphora is present.
121
- * Deliberately SEPARATE from normalizeQuery's own KIND_NOUN_ANAPHORA_RE
122
- * replace just above (: this never mutates its input and has no effect
123
- * on normalizeQuery's behavior, signature, or any of its many call sites.
124
- * A caller that needs BOTH the collapsed pronoun AND the kind it stood for
125
- * (chat.mjs's runAsk, at its pronoun-reuse site) calls this side-channel on
126
- * the same raw text handed to normalizeQuery — order between the two calls
127
- * doesn't matter, since this one only ever reads. */
56
+ /** Read-only probe: the ENTITY_TO_TYPE class named by a KIND_NOUN_ANAPHORA_RE
57
+ * match, or null. Lets a caller (chat.mjs's pronoun-reuse site) recover the
58
+ * kind the pronoun stood for without mutating normalizeQuery's own path. */
128
59
  export function kindNounAnaphoraHint(text) {
129
60
  const m = new RegExp(KIND_NOUN_ANAPHORA_RE.source, "i").exec(String(text || ""));
130
61
  return m ? (ENTITY_TO_TYPE[m[2].toLowerCase()] || null) : null;
131
62
  }
132
63
 
133
- // every relation verb phrase VERB_TO_KIND knows, as one alternation (longest-first
134
- // so a multi-word verb like "inherit from" wins over its own leading word "inherit"
135
- // appearing elsewhere) — feeds the DOES-X-VERB-ANYTHING-ELSE frame below, which needs
136
- // to recognize "does <subject> <ANY closed relation verb> anything/something [else]"
137
- // without hardcoding its own parallel verb list.
64
+ // Every relation verb phrase, as one longest-first alternation — feeds the
65
+ // DOES-X-VERB-ANYTHING-ELSE frame below without hardcoding a parallel list.
138
66
  const VERB_ALTERNATION = Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
139
67
 
140
- /** Just the curated MISSPELLINGS correction (ask-vocab.mjs), standalone for a
141
- * caller that needs typo-tolerant ANCHOR-WORD matching (a closed regex shape
142
- * keyed on a literal "what"/"which"/"where" etc.) without running the rest of
143
- * normalizeQuery's pipeline (contractions, preamble/subordination/conditional
144
- * frame rewrites, filler-word stripping) — those can restructure the sentence
145
- * in ways a shape-matcher never expects (0.9.14 Tier-2 playtest: normalizeQuery
146
- * turns "tell me about Controller" into "about Controller", which would break
147
- * chat.mjs's OWN "^tell me about …" shape regex if fed through wholesale). Pure,
148
- * idempotent, same table as normalizeQuery's own first correction step. */
68
+ /** Just the MISSPELLINGS correction, standalone, for a caller with its own
69
+ * anchor regex that wants typo tolerance without normalizeQuery's more
70
+ * invasive rewrites (frame rewrites can restructure the sentence in ways a
71
+ * shape-matcher doesn't expect). */
149
72
  export function correctMisspellings(text) {
150
73
  return String(text || "").replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
151
74
  }
152
75
 
153
- /** FILLER_WORDS (ask-vocab.mjs), pre-built into one alternation once at module
154
- * load — same "build the regex from the table once, reuse it" discipline as
155
- * every other closed-vocabulary regex in this file (CONTRACTION_RE,
156
- * MISSPELLING_RE, …), rather than rebuilding it inside stripFillerWords on
157
- * every call. */
158
76
  const FILLER_RE = FILLER_WORDS.length
159
77
  ? new RegExp(
160
78
  "\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b\\s*,?",
@@ -162,55 +80,23 @@ const FILLER_RE = FILLER_WORDS.length
162
80
  )
163
81
  : null;
164
82
 
165
- /** Just the filler/politeness-word strip (ask-vocab.mjs's FILLER_WORDS),
166
- * standalone the same "one piece of the pipeline, not the whole thing"
167
- * need correctMisspellings above exists for: a caller with its own
168
- * closed anchor regex (chat.mjs's moduleOrientLane matches "^what does
169
- * do$" itself) wants leading/embedded filler cleared WITHOUT running
170
- * normalizeQuery's other, more invasive rewrites (contraction expansion,
171
- * preamble/subordination/conditional frame rewrites) that can restructure
172
- * the sentence in ways its own shape-matcher never expects — the exact
173
- * risk correctMisspellings' own docblock describes for the same reason.
174
- *
175
- * A comma trailing the filler word/phrase itself (across any whitespace,
176
- * e.g. "um, like," or "quickly,") is swallowed WITH it — leading
177
- * conversational filler is routinely comma-spliced onto the real question
178
- * ("so um, like, what does X do exactly?"), and stripping only the word
179
- * leaves the comma stranded as parse-corrupting punctuation debris (a
180
- * leading "," defeats every `^`-anchored template downstream, both parse
181
- * strategies and lane-local regexes alike) — the same species of
182
- * leftover-debris bug the preamble frames elsewhere in this file
183
- * (GREETING_PREAMBLE_RE et al.) already avoid by consuming their own
184
- * delimiter. The trailing `,?` is safe to make unconditional (unlike the
185
- * preamble frames, no delimiter-required gate is needed): it only ever
186
- * fires immediately after a MATCHED filler word, so it can only ever eat a
187
- * comma that was already glued to filler, never a comma separating real
188
- * content ("the modules, and the classes" — that comma sits after the
189
- * content word "modules", not after any filler word). Pure, idempotent;
190
- * unmatched text passes through byte-unchanged. */
83
+ /** Just the filler/politeness-word strip, standalone, for a caller that wants
84
+ * filler cleared without normalizeQuery's more invasive rewrites. A comma
85
+ * trailing a stripped filler word is swallowed with it, so it never survives
86
+ * as debris that defeats a `^`-anchored template downstream. */
191
87
  export function stripFillerWords(text) {
192
88
  let q = String(text || "");
193
89
  if (FILLER_RE) q = q.replace(FILLER_RE, " ");
194
90
  return q.replace(/\s+/g, " ").trim();
195
91
  }
196
92
 
197
- // ---- closed PREAMBLE frames (0.8.2 feel wave, PLAN_CHAT_FEEL item 2) — the
198
- // conversational wrapping a developer puts AROUND a real question: a greeting
199
- // lead-in with a delimiter ("hey there, quick question - …"), a thanks lead-in
200
- // with a delimiter ("thanks so much, …" Bug B2, 0.8.2 follow-up), the modal
201
- // politeness wrapper ("can you … please"), and the show/give-me presentation
202
- // bridge. These are DELIMITER- and PHRASE-anchored, so they must run BEFORE the
203
- // FILLER-strip pass below: FILLER_WORDS strips "hey"/"can you" as bare words, so
204
- // after that pass the frames' anchors are gone while the punctuation debris
205
- // ("there, quick question -") still poisons the parse (the playtest wall).
206
- // Applied inside normalizeQuery — the one seam BOTH composition sites (ask.mjs
207
- // parseQuery and interpret/pipeline.mjs normalizeInput) run first — AFTER the
208
- // word-restoring correction tables (so "gimme"/"shwo me" are already "give me"/
209
- // "show me") and BEFORE the filler strip. Closed patterns, applied in order to a
210
- // small fixpoint; unmatched text passes through byte-unchanged. ----
211
-
212
- /** Any relation verb phrase from the shared vocabulary — the show/give-me
213
- * bridge's "this remainder is a real relation query" probe. */
93
+ // ---- closed PREAMBLE frames: conversational wrapping around a real question
94
+ // (greeting/thanks lead-ins, modal politeness, show/give-me). Delimiter- and
95
+ // phrase-anchored, so they run before the filler strip erases their anchor
96
+ // words; applied to a small fixpoint. Unmatched text passes through unchanged.
97
+
98
+ /** Any relation verb phrase the show/give-me bridge's "is the remainder a
99
+ * real relation query" probe. */
214
100
  const RELATION_VERB_RE = new RegExp(
215
101
  "\\b(?:" + Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
216
102
  "i",
@@ -234,183 +120,51 @@ const isListingRemainder = (rest) => {
234
120
  };
235
121
 
236
122
  /** Greeting lead-in with a delimiter (+ optional "quick question" bridge):
237
- * "hey there, quick question - <Q>" -> "<Q>". The delimiter and the non-empty
238
- * remainder are REQUIRED, so a bare "hey there" stays a greeting for chat's
239
- * conversational lane, and "hey tmct, …" (a vocative, no delimiter after the
240
- * greeting word) is left for the noise-strip tier that already owns it. */
241
- // "g'day"/"gday" (AU/NZ dialect, §3b): the same lead-in-with-delimiter shape as
242
- // hi/hey/howdy — 0.9.14 Tier-2 playtest §3b spot-check found "g'day, what does
243
- // Base contain" fell through to a bogus "'g'day Base' matches more than one
244
- // module" object search instead of stripping the greeting.
245
- // "good morning"/"good afternoon"/"good evening"/"good day"/"greetings"/
246
- // "salutations" (formal register, §3b) — chat.mjs's own bare-turn GREETINGS/
247
- // IDENTITY_PHRASES closed sets already recognize these exact phrases stand-
248
- // alone, but this LEAD-IN regex (a greeting fused onto a real question in the
249
- // same turn) didn't carry the multi-word formal forms at all: a second Tier-2
250
- // playtest pass (0.9.14) found "Good day, what is a method" hit the grammar
251
- // wall outright, and "Good morning, what about tests" fell through to a bogus
252
- // "no module matching 'Good morning' found" object search — the exact same
253
- // failure g'day had before its own fix, just for the formal register instead
254
- // of the AU/NZ dialect. Formal register also plausibly types the lead-in as
255
- // its OWN full sentence ("Good day. What is a method?") rather than a comma
256
- // splice — "." joins the delimiter class for exactly this reason; a bare
257
- // greeting alone ("hi.") still can't match since the regex also requires a
258
- // non-empty remainder AFTER the delimiter.
259
- // "yeah nah" (AU/NZ informal opener, §3b, Tier 6 playtest): a soft discourse
260
- // filler AU/NZ speakers lead a sentence with, distinct from an actual "no" —
261
- // chat.mjs's own GREET closed set already recognizes the BARE phrase, but a
262
- // fused lead-in ("yeah nah, what does the router do") had no preamble form at
263
- // all and fell straight to the raw grammar wall, the exact failure g'day's own
264
- // fix (just above) closed for that dialect's greeting word.
265
- // "howdy pardner" (Tier 6 playtest, §3b dialect axis): "howdy" alone already
266
- // matched, but a VOCATIVE word right after it ("pardner", a US-Western/cowboy
267
- // register touch) sat between the greeting and its delimiter, where only the
268
- // literal word "there" was tolerated — "howdy pardner, remember that X" fell
269
- // straight to the raw grammar wall. A small closed vocative set, same
270
- // discipline as the greeting word list itself.
123
+ * "hey there, quick question - <Q>" -> "<Q>". Delimiter and non-empty
124
+ * remainder are both required, so a bare greeting stays small-talk. */
271
125
  const GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy|g'?day|yeah\s+nah|good\s+(?:morning|afternoon|evening|day)|greetings|salutations)(?:\s+(?:there|pardner|folks|friend|mate))?\s*[,.—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
272
- /** Thanks lead-in with a delimiter (+ optional "quick question" bridge), the
273
- * sibling of GREETING_PREAMBLE_RE for the "thanks" word family (Bug B2, 0.8.2
274
- * follow-up): "thanks, <Q>" / "thanks so much, <Q>" -> "<Q>". chat.mjs's
275
- * GREETINGS set already treats a BARE "thanks"/"thank you"/"cheers" as
276
- * small-talk, and noise-strip.mjs's CASCADE_NOISE strips a single bare
277
- * token — but a multi-word lead-in ("thanks so much, X", "thanks a lot, X")
278
- * left "so"/"much"/"a"/"lot" debris after the noise strip that corrupted
279
- * re-parse (the object term inherited the debris). Same delimiter- and
280
- * non-empty-remainder-REQUIRED discipline as the greeting frame, so a bare
281
- * "thanks so much" (no delimiter, no question) stays small-talk. */
126
+ /** Thanks lead-in with a delimiter, the sibling of GREETING_PREAMBLE_RE for
127
+ * the "thanks" word family: "thanks so much, <Q>" -> "<Q>". */
282
128
  const THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(?:\s+(?:so\s+much|a\s+lot|very\s+much|a\s+bunch))?\s*[,—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
283
- /** An ACKNOWLEDGEMENT lead-in with a delimiter, the sibling of GREETING_PREAMBLE_RE/
284
- * THANKS_PREAMBLE_RE for the "ok"/"cool" word family (Tier 6 playtest: "ok cool,
285
- * what about the TaskController" — a drill-down continuation politely
286
- * acknowledging the PREVIOUS answer before asking the next question). chat.mjs's
287
- * OK_ACK set already treats a BARE "ok"/"cool"/"sounds good" as small-talk, but a
288
- * CHAINED lead-in ("ok cool, X") left "cool"/"," debris ahead of the real
289
- * question — same failure class GREETING/THANKS' own multi-word lead-ins had.
290
- * The marker group repeats (`+`) so a stack of ack-words peels in one pass ("ok
291
- * cool" both go), each followed by whitespace/comma; same delimiter- and
292
- * non-empty-remainder-REQUIRED discipline as the two frames above. */
293
- // "no worries" (Tier 6 playtest, §3b dialect axis, AU/NZ): a casual "that's
294
- // fine"/"no problem" opener that also, like the other ack words, sometimes
295
- // leads straight into the NEXT question rather than standing alone. "aight"
296
- // (cycle 5, §3b typo/elongation axis): the further-dropped texting-register
297
- // contraction of "alright", chainable with the others just like "ok cool,".
129
+ /** Acknowledgement lead-in with a delimiter ("ok cool, <Q>"), repeating (`+`)
130
+ * so a stack of ack-words peels in one pass. */
298
131
  const ACK_PREAMBLE_RE = /^(?:(?:ok(?:ay)?|aight|cool|alright|sure|right|fine|great|nice|got it|gotcha|sounds good|no worries|no problem)[\s,]+)+(.+)$/i;
299
- /** A SELF-ORIENTATION lead-in with a delimiter — "just poking around, what's
300
- * in this repo", "just browsing, X", "just exploring, X" (Tier 6 playtest,
301
- * §3: the vague-opener family a genuine first-time stranger types). Same
302
- * delimiter-required discipline as GREETING/THANKS/ACK_PREAMBLE_RE above —
303
- * a bare "just poking around" with no question stays small-talk (this file
304
- * never claims a turn that has no remainder to hand back).
305
- * HANDOVER.md 2026-07-10 item 3: "first time trying this out"/"first time
306
- * using this"/"first time here" is the SAME self-orientation species — a
307
- * genuine stranger's opener, just phrased around their own inexperience
308
- * rather than what they're doing right now — found live as "hey, first
309
- * time trying this out - what is in here?" falling straight to the raw
310
- * grammar wall (GREETING_PREAMBLE_RE peels "hey,", but nothing recognized
311
- * the remainder as a preamble at all). Added as a sibling alternative in
312
- * the SAME regex/capture group, so it strips into the identical downstream
313
- * shape ("just poking around, X" and "first time trying this out, X" both
314
- * hand back the bare "X" for the ordinary pipeline to answer) rather than a
315
- * new frame with its own behavior. */
132
+ /** Self-orientation lead-in with a delimiter — "just poking around, <Q>",
133
+ * "first time using this, <Q>". */
316
134
  const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here))\s*[,.—–-]\s*(.+)$/i;
317
- /** A repeated leading HEDGE ADVERB ("maybe", "possibly", "perhaps") ahead of a
318
- * polite request verb the sibling of ACK_PREAMBLE_RE for HEDGING rather than
319
- * acknowledging (Tier 6 playtest §3's own stacked-politeness example: "could
320
- * you maybe possibly tell me... what saveStore does"). Unlike ACK_PREAMBLE_RE,
321
- * no delimiter is required — a hedge adverb modifies the verb it precedes
322
- * directly ("maybe possibly tell me"), so requiring a comma would miss the
323
- * common case; unconditional strip, same as the other preamble frames (none
324
- * of these three words is grammar-owned vocabulary). Deliberately a narrow,
325
- * closed three-word set. */
135
+ /** Repeated leading hedge adverb before a polite request verb ("maybe
136
+ * possibly tell me <Q>"). No delimiter required, unlike ACK_PREAMBLE_RE. */
326
137
  const HEDGE_ADVERB_PREAMBLE_RE = /^(?:(?:maybe|possibly|perhaps)\s+)+(.+)$/i;
327
- /** A floating "if it's not too much trouble"/"if that's not too much bother"
328
- * aside a genuine mid-sentence PARENTHETICAL (unlike every other frame
329
- * here, this is NOT anchored to the start of the string), stripped wherever
330
- * it appears, comma-bounded on either side. Tier 6 playtest's own example
331
- * phrase: "...tell me, if its not too much trouble, what saveStore does". */
138
+ /** A floating "if it's not too much trouble" aside a mid-sentence
139
+ * parenthetical, stripped wherever it appears (not start-anchored). */
332
140
  const TROUBLE_ASIDE_RE = /,?\s*if\s+(?:it'?s|it\s+is|that'?s|that\s+is)\s+not\s+too\s+much\s+(?:trouble|bother|hassle)\s*,?\s*/i;
333
- /** Modal politeness wrapper: "can/could/would/will you [please] <Q>[, please][?]"
334
- * -> "<Q>". FILLER_WORDS already ate "can you"/"please" as words; this frame
335
- * removes them as a WRAPPER so the ", please" comma never survives into the
336
- * parsed object term. The unwrapped remainder flows on through the ordinary
337
- * passes, so "can you tell me a joke" -> "tell me a joke" -> (FILLER) "a joke"
338
- * — byte-identical to what the bare form normalizes to (the hm-joke wall). */
141
+ /** Modal politeness wrapper: "can/could/would/will you [please] <Q>" -> "<Q>". */
339
142
  const MODAL_WRAPPER_RE = /^(?:can|could|would|will)\s+you\s+(?:please\s+)?(.+?)(?:[,\s]+please)?\??$/i;
340
- /** "explain" politeness/ESL wrapper: "explain [to me|please]* <Q>" -> "<Q>"
341
- * (0.9.13 Tier-1 playtest, §3b surface-variation axis) a formal/ESL lead-in
342
- * around an ordinary structural question ("explain please where is it
343
- * defined") used to leave "explain"/"please" as noise words that corrupted the
344
- * object term into a bogus search ("no module matching 'explain it' found").
345
- * Anchored to an INTERROGATIVE remainder (same guard as the show/give-me
346
- * bridge below) so this frame only unwraps a real WH-question underneath —
347
- * chat.mjs's own IDENTITY_PHRASES ("explain what is this") and the bare
348
- * "explain" elaboration request (WHY set) are matched on the RAW turn text
349
- * before normalizeQuery ever runs, so neither is touched by this frame. */
143
+ /** "explain [to me|please]* <Q>" -> "<Q>", gated on an interrogative
144
+ * remainder so it only unwraps a real WH-question underneath. */
350
145
  const EXPLAIN_WRAPPER_RE = /^explain\s+(?:to\s+me\s+|please\s+)*(.+?)\??$/i;
351
- /** "tell me <Q>" (bare, no "about") -> "<Q>" the sibling of EXPLAIN_WRAPPER_RE
352
- * for the "tell me" verb family when what follows is already a full WH-question
353
- * rather than a bare noun. "tell me about X" is vagueTouchTermOf's own separate
354
- * territory (chat.mjs) and is untouched here: this frame's interrogative-lead
355
- * gate can only ever fire on a remainder that "about X" never satisfies (it
356
- * starts with "about", not a WH-word). Found live (Tier 6 playtest): "could you
357
- * maybe possibly tell me... what saveStore does" left "tell me what saveStore
358
- * does" needing one more unwrap after HEDGE_ADVERB_PREAMBLE_RE and
359
- * MODAL_WRAPPER_RE peeled their own layers. */
146
+ /** "tell me <Q>" (bare, no "about") -> "<Q>"; "tell me about X" is a
147
+ * different, untouched territory (chat.mjs's vagueTouchTermOf). */
360
148
  const TELL_ME_WRAPPER_RE = /^tell\s+me\s+(.+?)\??$/i;
361
- /** show/give-me presentation bridge: "show me [the] <thing>". Three-way:
362
- * a KIND-listing remainder is left untouched (the compositional list grammar
363
- * owns "show me untested modules"); a remainder carrying a relation verb or an
364
- * interrogative lead is a real query merely presented — unwrap to it ("show me
365
- * which modules import X" -> "which modules import X"); anything else is an
366
- * entity presentation — bridge to the describe surface ("show me the store
367
- * module" -> "describe store module"). */
149
+ /** show/give-me presentation bridge: a kind-listing remainder is left
150
+ * untouched, a relation/interrogative remainder unwraps to itself, anything
151
+ * else bridges to "describe <thing>". */
368
152
  const SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
369
153
 
370
154
  /** Leading STACCATO connective before an ALREADY well-formed question ("and
371
- * what imports it", "so does it import anything else", "also where is X
372
- * defined") -> the question alone (0.9.15 Tier-1 single-touch playtest). A
373
- * rapid-fire drill-down naturally opens its next turn with a bare discourse
374
- * connective (chat.mjs's own STACCATO_LEAKED_CONNECTIVES set: and/also/so/
375
- * then/now, here joined by "but" for the same family); when what follows is
376
- * ALREADY an interrogative lead or an auxiliary-question opener, the
377
- * connective carries no query content of its own — conversational
378
- * scaffolding, same species as the greeting/subordination preambles. Gated
379
- * on the REMAINDER starting a real question, so a genuine mid-clause boolean
380
- * composition ("classes that inherit from Base and are tested") is never
381
- * touched — that "and" never sits at position 0 to begin with. Without this,
382
- * "and what imports it" parsed as an "ask"-shape question with "and" itself
383
- * MISREAD as the subject term — a miss the relaxation cascade can't rescue,
384
- * because "and" is protected CONTENT_VOCAB (a boolean connective elsewhere)
385
- * and the noise-strip layer never drops content vocab. */
155
+ * what imports it") -> the question alone. Gated on the remainder starting
156
+ * a real question, so a mid-clause boolean composition ("classes that
157
+ * inherit from Base and are tested") is never touched. Without this gate,
158
+ * "and" itself got misread as the subject term — a miss the relaxation
159
+ * cascade can't rescue, since "and" is protected content vocabulary the
160
+ * noise-strip layer never drops. */
386
161
  const LEADING_CONNECTIVE_RE = /^(?:and|also|so|then|now|but)\s+(.+)$/i;
387
162
  const QUESTION_AUX_LEAD_RE = /^(?:does|do|did|is|are|was|were|has|have|had|can|could|will|would|should)\b/i;
388
163
 
389
- /** A TOPIC-SWITCH / self-interruption preamble "actually never mind, what
390
- * calls X", "no wait, I meant what calls Y", "hold on, where is Z defined"
391
- * (Tier 6 playtest, §3: "no wait", mid-conversation topic switches). A real
392
- * user abandoning whatever they were about to say and asking something else
393
- * instead — same species as LEADING_CONNECTIVE_RE just above (a discourse
394
- * marker carrying no query content of its own, gated on a real question
395
- * following it), just a richer closed marker set than a single connective
396
- * word, and CHAINABLE ("actually" + "never mind" + "i meant" can all stack —
397
- * the fixpoint loop below peels one per pass). Deliberately NOT the same
398
- * mechanism as SELF_CORRECTION_RE (this file, further down): that shape
399
- * requires an explicit "sorry"/"i mean" marker WITH a mandatory trailing
400
- * delimiter, modeling a mid-sentence restart of the SAME clause with real
401
- * text on both sides; this one is a STANDALONE marker at the very start of
402
- * the turn with nothing meaningful before it, and the delimiter (a comma) is
403
- * optional — colloquial speech routinely drops it ("no wait i meant …"). The
404
- * marker group REPEATS (`+`, mirroring ACK_PREAMBLE_RE just above) so a stack
405
- * of markers peels in ONE pass ("actually" + "never mind," both go) — unlike
406
- * LEADING_CONNECTIVE_RE's single-word frame, gating on the remainder after
407
- * only the FIRST marker would reject the strip before later markers ever get
408
- * a chance to peel (found live: "actually never mind, X" left "never mind,
409
- * X" as the gated remainder, which itself never looks like a question lead,
410
- * so the whole frame silently declined). None of these markers is grammar-
411
- * owned vocabulary (VERB_TO_KIND/ENTITY_TO_TYPE), so — same as GREETING_
412
- * PREAMBLE_RE/THANKS_PREAMBLE_RE/ACK_PREAMBLE_RE above — the strip is
413
- * unconditional; no interrogative-lead gate needed. */
164
+ /** A topic-switch/self-interruption preamble ("actually never mind, <Q>"),
165
+ * repeating so a stack of markers peels in one pass. Distinct from
166
+ * SELF_CORRECTION_RE below, which needs an explicit "sorry"/"i mean" marker
167
+ * with a mandatory trailing delimiter and models a mid-clause restart. */
414
168
  const TOPIC_SWITCH_PREAMBLE_RE =
415
169
  /^(?:(?:actually|no\s+wait|wait|hold\s+on|never\s+mind|scratch\s+that|on\s+second\s+thought|i\s+mean(?:t)?)[\s,.]+)+(.+)$/i;
416
170
 
@@ -419,9 +173,6 @@ const TOPIC_SWITCH_PREAMBLE_RE =
419
173
  * please") peel fully. Pure and idempotent; unmatched text passes through. */
420
174
  export function applyPreambleFrames(text) {
421
175
  let q = String(text || "");
422
- // The trouble-aside is a mid-sentence parenthetical, not a start-anchored
423
- // wrapper — stripped unconditionally, once, before the fixpoint loop (it
424
- // never interacts with the other frames' anchoring).
425
176
  q = q.replace(TROUBLE_ASIDE_RE, " ").replace(/\s+/g, " ").trim();
426
177
  for (let pass = 0; pass < 3; pass++) {
427
178
  const before = q;
@@ -453,17 +204,9 @@ export function applyPreambleFrames(text) {
453
204
  m = q.match(LEADING_CONNECTIVE_RE);
454
205
  if (m) {
455
206
  const rest = m[1].trim();
456
- // Tier 6 playtest cycle 5: a leading connective ("so") sandwiched
457
- // BETWEEN two other discourse markers ("aight cool, so actually wait,
458
- // what calls X") used to block the WHOLE fixpoint — the remainder right
459
- // after "so" ("actually wait, X") isn't ITSELF a question yet (it still
460
- // has its own marker prefix), so the original interrogative-only gate
461
- // rejected the strip, and TOPIC_SWITCH_PREAMBLE_RE (anchored to start)
462
- // never got a chance at "actually" while "so " still sat in front of
463
- // it. Also accepting a remainder that matches one of this file's OWN
464
- // other closed preamble frames is safe by the same logic those frames
465
- // already rely on: each is anchored + closed-vocabulary, so a match
466
- // here guarantees the NEXT pass strips it too, not a guess.
207
+ // Also accept a remainder matching another closed preamble frame, so a
208
+ // connective sandwiched between two markers ("so actually wait, X")
209
+ // doesn't block the fixpoint.
467
210
  if (
468
211
  INTERROGATIVE_LEAD_RE.test(rest) || QUESTION_AUX_LEAD_RE.test(rest)
469
212
  || TOPIC_SWITCH_PREAMBLE_RE.test(rest) || ACK_PREAMBLE_RE.test(rest)
@@ -475,47 +218,12 @@ export function applyPreambleFrames(text) {
475
218
  return q;
476
219
  }
477
220
 
478
- // ---- ADVANCED_GRAMMAR track (a) (PLAN_ADVANCED_GRAMMAR.md §2): closed-frame
479
- // subordination + conditionals — the proven 0.8.2 preamble-frame method (closed,
480
- // delimiter-anchored, first-match-wins, unmatched text passes through
481
- // byte-unchanged) at its next size up. Two families:
482
- // SUBORDINATION_FRAMES strippable leading framing clauses ("since we're
483
- // refactoring, which modules import x?" -> "which modules import x?") —
484
- // the clause carries no query content, it's conversational scaffolding
485
- // around a real question, same species as the greeting/thanks preambles.
486
- // CONDITIONAL frames "if <clause>, is it <qualifier>?" compiles to the
487
- // EXISTING compositional boolean-qualifier shape the grammar already
488
- // answers ("<kind> <relation-gerund> <object> and <qualifier>" — proven by
489
- // test/ask-compositional.test.mjs's "classes inheriting from Base and
490
- // tested"), and the counterfactual "if X were deleted, what would break"
491
- // compiles to the existing transitive-modifier reverse-dependency closure
492
- // ("which modules transitively import X" — proven by
493
- // test/ask.test.mjs's transitive-modifier suite). Both frame families are
494
- // wired INSIDE normalizeQuery (not as a separate call site) because this
495
- // agent's scope is normalize.mjs only — ask.mjs/interpret/pipeline.mjs
496
- // call normalizeQuery already, so embedding here reaches every strategy
497
- // for free, with no new call site required. A conditional shape NOT
498
- // covered by these two closed patterns is deliberately left unmatched —
499
- // the honest-miss discipline PLAN_ADVANCED_GRAMMAR §2a states explicitly
500
- // ("refuse any conditional whose consequent isn't a computable
501
- // traversal"): only rewrite to a shape independently verified correct,
502
- // never a plausible-looking guess. ----
503
-
504
221
  /** Strippable leading framing clause: "since/although/though/while/because/
505
- * whereas/given that/now that <clause>, <Q>" -> "<Q>". Delimiter- (comma-)
506
- * anchored and non-empty-remainder-required, same discipline as
507
- * GREETING_PREAMBLE_RE — a bare "since when do you know that" (no comma
508
- * splitting a framing clause from a real question) is NOT a subordination
509
- * wrapper and is left alone; "since" as an ordinary temporal content word
510
- * ("modules changed since last week", no leading comma-delimited clause)
511
- * never matches either. */
222
+ * whereas/given that/now that <clause>, <Q>" -> "<Q>". Comma-anchored and
223
+ * non-empty-remainder-required, same discipline as GREETING_PREAMBLE_RE. */
512
224
  const SUBORDINATION_FRAMES_RE =
513
225
  /^(?:since|although|though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
514
226
 
515
- /** Apply the subordination-frame strip to a small fixpoint (a doubly-wrapped
516
- * "well, since X, although Y, <Q>" peels fully — rare, but the same
517
- * discipline applyPreambleFrames already uses). Pure; unmatched text passes
518
- * through byte-unchanged. */
519
227
  export function applySubordinationFrames(text) {
520
228
  let q = String(text || "");
521
229
  for (let pass = 0; pass < 3; pass++) {
@@ -526,40 +234,15 @@ export function applySubordinationFrames(text) {
526
234
  return q;
527
235
  }
528
236
 
529
- // ---- SELF-CORRECTION (found live, playtest sprint round 5, HANDOVER item 12.1): a
530
- // mid-sentence false start, abandoned and restarted "what -- sorry, who inherits
531
- // from Record", "what is a class -- i mean, what is a module". Same species as the
532
- // preamble/subordination frames above (closed, delimiter-anchored, first-match-wins,
533
- // unmatched text passes through byte-unchanged), sized for a false-start clause
534
- // instead of a leading wrapper clause. The delimiter is the marker phrase "sorry" or
535
- // "i mean" — REQUIRED, with an optional interruption dash ("--"/"—"/"-") in front of
536
- // it and a required trailing separator (dash/comma/colon) after it. Deliberately does
537
- // NOT match on a bare dash alone with no marker word: an ordinary em-dash aside
538
- // ("modules — like Base — that inherit from X") is common prose, not a restart, and
539
- // treating every dash as a delimiter would be a guess this file's discipline forbids
540
- // everywhere else.
541
- //
542
- // Tier 6 playtest tried making the trailing delimiter optional too (to catch
543
- // "what calls listTasks -- oh wait, i mean createTask" — no comma after "i
544
- // mean") and reverted it live: this regex's `.+?` prefix discards EVERYTHING
545
- // before the marker, which is correct for a FULL-CLAUSE restart (the remainder
546
- // is a complete new question, verb included, e.g. "which classes inherit from
547
- // Base") but wrong for an OBJECT-ONLY restart, where the verb clause ("what
548
- // calls") must survive and only the object swaps. Without the delimiter, this
549
- // object-only shape reduced to the bare noun "createTask" alone (no verb at
550
- // all) — a genuine regression from the honest "did you mean listTasks or
551
- // createTask?" ambiguity nudge the UNCHANGED regex already gives (a real
552
- // candidate list including the correct answer is an acceptable FLOW under
553
- // SKILL_CHAT_PLAYTEST.md §0/§2, not a dead end) to a hard wall. Fixing the
554
- // object-only case properly needs verb-clause-preserving logic this closed,
555
- // delimiter-anchored frame mechanism isn't shaped for — left as a genuine,
556
- // narrower ceiling rather than risk widening this proven, tested regex. ----
237
+ /** A mid-sentence false start, abandoned and restarted: "what -- sorry, who
238
+ * inherits from Record". The marker ("sorry"/"i mean") and its trailing
239
+ * delimiter are both required an ordinary em-dash aside is common prose,
240
+ * not a restart, and treating every dash as a delimiter would be a guess.
241
+ * The trailing delimiter also means an object-only restart with no comma
242
+ * after "i mean" isn't rescued here; that's a narrower, accepted ceiling. */
557
243
  const SELF_CORRECTION_RE =
558
244
  /^.+?(?:\s*(?:--|—|-)\s*)?\b(?:sorry|i\s+mean)\b\s*(?:--|—|-|,|:)\s*(.+)$/i;
559
245
 
560
- /** Apply the self-correction strip to a small fixpoint (a stacked restart —
561
- * "what -- sorry, who -- sorry, what inherits from Record" — peels to the final
562
- * restart). Pure; unmatched text passes through byte-unchanged. */
563
246
  export function applySelfCorrectionFrames(text) {
564
247
  let q = String(text || "");
565
248
  for (let pass = 0; pass < 3; pass++) {
@@ -572,11 +255,9 @@ export function applySelfCorrectionFrames(text) {
572
255
  return q;
573
256
  }
574
257
 
575
- /** relation-verb (bare 3rd-person singular, the RELATIONS table's own primary
576
- * form) -> gerund, the shape the compositional grammar's proven
577
- * "<kind> <gerund> <object> and <qualifier>" pattern needs. A small, closed,
578
- * hand-curated table (not a generic morphological rule) — the same
579
- * "no guessing" discipline as every other closed vocabulary in this file. */
258
+ /** relation-verb -> gerund, the shape the compositional grammar's
259
+ * "<kind> <gerund> <object> and <qualifier>" pattern needs. A small,
260
+ * hand-curated table, not a morphological rule. */
580
261
  const CONDITIONAL_VERB_GERUND = Object.freeze({
581
262
  imports: "importing", calls: "calling", touches: "touching", tests: "testing",
582
263
  exports: "exporting", contains: "containing", defines: "defining", uses: "using",
@@ -606,21 +287,13 @@ const CONDITIONAL_QUALIFIER_RE = new RegExp(
606
287
  "i",
607
288
  );
608
289
 
609
- /** Counterfactual deletion: "if <X> were/was deleted/removed(,)? what
610
- * would/might break/fail/be affected?" -> "which modules transitively import
611
- * <X>" the EXISTING reverse-dependency closure (impactClosure via the
612
- * transitive modifier, ask.mjs/codegraph.mjs), proven correct by
613
- * test/ask.test.mjs's transitive-modifier suite. Exported so chat.mjs can
614
- * independently recognize the SAME raw query shape and prepend a
615
- * hypothetical marker to the rendered answer (a hypothetical consequent must
616
- * never be presented as an unqualified fact) — normalize.mjs only rewrites
617
- * the QUESTION text, it never touches the answer. */
290
+ /** Counterfactual deletion -> "which modules transitively import <X>" (the
291
+ * existing reverse-dependency closure). Exported so chat.mjs can recognize
292
+ * the same shape and prepend a hypothetical marker to the rendered answer. */
618
293
  export const COUNTERFACTUAL_RE =
619
294
  /^if\s+(.+?)\s+(?:were|was)\s+(?:deleted|removed),?\s*what\s+(?:would|might|could)\s+(?:break|fail|be\s+affected)\??$/i;
620
295
 
621
- /** Apply the two closed CONDITIONAL frames, first-match-wins (qualifier
622
- * composition tried first — it is the more specific shape). Pure; unmatched
623
- * text passes through byte-unchanged. */
296
+ /** Apply the two conditional frames, qualifier composition first (more specific). */
624
297
  export function applyConditionalFrames(text) {
625
298
  const q = String(text || "");
626
299
  const qual = q.match(CONDITIONAL_QUALIFIER_RE);
@@ -636,36 +309,23 @@ export function applyConditionalFrames(text) {
636
309
 
637
310
  /** Free-text -> normalized free-text: contractions expanded, g-dropped words
638
311
  * restored, closed preamble/subordination/conditional frames peeled, filler/
639
- * politeness words stripped. Idempotent and pure the same
640
- * input always normalizes the same way, so both parsing strategies see
641
- * identical text and their outputs are directly comparable. Deliberately
642
- * does NOT force lowercase: object/subject terms (module names like
643
- * "myFile", class names like "Base") are meaningfully cased, and every
644
- * substitution below already matches case-insensitively (`i`/`gi` flags) —
645
- * forcing the whole string to lowercase would silently corrupt every parsed
646
- * term's case instead. */
312
+ * politeness words stripped. Idempotent and pure. Deliberately does NOT
313
+ * force lowercase object/subject terms (module/class names) are
314
+ * meaningfully cased, and every substitution already matches
315
+ * case-insensitively. */
647
316
  export function normalizeQuery(text) {
648
317
  let q = String(text || "");
649
318
  q = q.replace(CONTRACTION_RE, (m) => CONTRACTIONS[m.toLowerCase()]);
650
319
  q = q.replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
651
320
  q = q.replace(WRONG_WORD_RE, (m) => WRONG_WORDS[m.toLowerCase()]);
652
- // "w/" -> "with", leetspeak "4" -> "for" (narrow trigger shapes only) — see
653
- // the two tables' own docblocks just above for why neither can live in the
654
- // MISSPELLINGS/WRONG_WORDS tables above this function.
655
321
  q = q.replace(W_SLASH_RE, "with");
656
322
  q = q.replace(FOR_DIGIT_THANKS_RE, (_, w) => `${w} for`);
657
323
  q = q.replace(FOR_DIGIT_EXAMPLE_RE, (_, w) => `for ${w}`);
658
324
  q = q.replace(KIND_NOUN_ANAPHORA_RE, (_, pron) => pron);
659
325
  q = q.replace(G_DROP, "$1ing");
660
- // closed preamble frames (greeting lead-in, modal wrapper, show/give-me
661
- // bridge) — AFTER the correction tables (a repaired "give me"/"show me" still
662
- // feeds the bridge) but BEFORE the filler strip erases their anchor words.
663
326
  q = applyPreambleFrames(q);
664
- // self-correction (strip an abandoned false-start clause) BEFORE subordination/
665
- // conditional: a false start can itself look like the OPENING of a subordination
666
- // clause ("since -- sorry, which modules import X" — "since" would otherwise be
667
- // read as SUBORDINATION_FRAMES_RE's own anchor), so peeling the restart first
668
- // means the real remainder is all either frame ever sees.
327
+ // self-correction runs before subordination/conditional: a false start can
328
+ // itself look like a subordination clause's opening ("since -- sorry, X").
669
329
  q = applySelfCorrectionFrames(q);
670
330
  // subordination (strip a leading framing clause) THEN conditional (compile
671
331
  // "if …" to an existing working shape) — subordination first so a stacked
@@ -686,13 +346,9 @@ export function normalizeQuery(text) {
686
346
  return q.replace(/\s+/g, " ").trim();
687
347
  }
688
348
 
689
- /** Recognized rhetorical/idiomatic constructions rewritten to the canonical form
690
- * of the SAME question before either parse strategy sees the text — a small
691
- * closed pattern set, not a general rewriter. Two families, tried in order:
692
- * COMMIT_CONTENT_FRAMES first ("what was in commit <sha>" -> "what did <sha>
693
- * touch"; sha-anchored, so it can't swallow a containment question), then the
694
- * §3.6 negative-rhetorical NEGATION_FRAMES. First matching frame across both wins
695
- * and rewriting stops; unmatched text passes through unchanged. */
349
+ /** Two closed families tried in order: COMMIT_CONTENT_FRAMES ("what was in
350
+ * commit <sha>" -> "what did <sha> touch"), then NEGATION_FRAMES. First
351
+ * match wins; unmatched text passes through unchanged. */
696
352
  export function applyNegationFrames(text) {
697
353
  for (const frame of [...COMMIT_CONTENT_FRAMES, ...NEGATION_FRAMES]) {
698
354
  const m = text.match(frame.re);
@@ -701,52 +357,27 @@ export function applyNegationFrames(text) {
701
357
  return text;
702
358
  }
703
359
 
704
- // ---- phrasing frames (SKILL_CHAT_PLAYTEST drill-down loop) route the natural
705
- // ways a developer asks a MEMBERS-of-class or a WHERE-DEFINED question onto the
706
- // canonical shapes the grammar already answers, so a phrasing miss becomes a real
707
- // answer (or an honest empty with a receipt) instead of the grammar wall. Same
708
- // closed-pattern, first-match-wins discipline as the negation/commit frames: each
709
- // frame REWRITES the whole line to a canonical query BOTH parse strategies then
710
- // handle for free. Run AFTER applyNegationFrames so a sha "what's in <sha>" is
711
- // already the commit-subject question before the members frame could see it. ----
360
+ // Phrasing frames: route natural phrasings of a members-of-class or
361
+ // where-defined question onto the canonical shape the grammar answers.
362
+ // First match wins; run after applyNegationFrames.
712
363
  export const PHRASING_FRAMES = Object.freeze([
713
364
  // MEMBERS-of-class → "what does X contain".
714
- // "what functions are in Task", "what methods are inside X", "what attributes are in X"
715
365
  { re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:are|is)\s+(?:in|inside|within)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
716
- // "what functions does Task have", "what methods does X have"
717
366
  { re: /^what\s+(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:does|do)\s+(.+?)\s+have\??$/i, to: (m) => `what does ${m[1]} contain` },
718
- // "what are the members of X", "what are the methods in X"
719
367
  { re: /^what\s+are\s+(?:the\s+)?(?:functions?|methods?|members?|attributes?|fields?|properties)\s+(?:of|in|inside|within)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
720
- // "members of X", "methods of X", "contents of X"
721
368
  { re: /^(?:the\s+)?(?:members?|methods?|attributes?|contents)\s+of\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
722
- // "what's in X" / "what is in X" (contraction already expanded; sha handled above)
723
369
  { re: /^what\s+is\s+(?:in|inside)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
724
- // "what else is in X" (0.9.15 Tier-1 single-touch playtest) the natural
725
- // "besides what I already know" drill-down after a members-of-class answer.
726
- // Distinct from the "what else does X <verb>" family (which the compositional
727
- // grammar already tolerates, dropping "else" as noise on its own): the "is
728
- // in" idiom is NOT a compositional marker, so parseComposite never sees it and
729
- // "what else is in X" fell through to the strategies with NO candidate at all
730
- // (neither recognizes the bare "is in" idiom once "else" sits in front of it).
731
- // The only rescue was the relaxation cascade's drop-unmatched layer — but that
732
- // layer refuses to accept a relaxed reading that still renders an honest EMPTY
733
- // (by design: relaxation must turn a miss into a real answer, never into
734
- // another kind of miss), so a genuinely empty class ("what else is in
735
- // Task.complete" — a method, no members) bottomed out at the bare grammar
736
- // wall instead of the specific "no contains edges" receipt. Routing this
737
- // frame onto the SAME direct "what does X contain" path the plain "what is
738
- // in X" frame above already uses sidesteps the cascade's conservative gate
739
- // entirely, so a real empty is reported honestly instead of walled.
370
+ // "what else is in X" drill-down after a members-of-class answer.
740
371
  { re: /^what\s+else\s+is\s+(?:in|inside)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `what does ${m[1]} contain` },
741
372
 
742
373
  // WHERE-DEFINED → "where is X defined". PAST TENSE ONLY ("what defined X", "what
743
374
  // declared X"): the PRESENT "what defines X" already parses as a reverse-defines
744
- // query (the module defining symbol X test/ask.test.mjs pins that), so rewriting
745
- // it would change that receipt. The past-tense form is the one that hit the wall.
375
+ // query (the module defining symbol X), so rewriting it would change that
376
+ // receipt. The past-tense form is the one that hit the wall.
746
377
  { re: /^what\s+(?:defined|declared)\s+(?:the\s+)?(?:function\s+|method\s+|class\s+|module\s+|variable\s+|constant\s+)?(.+?)\??$/i, to: (m) => `where is ${m[1]} defined` },
747
378
  // "where's X defined" (the "where's" contraction is not in the contraction table)
748
379
  { re: /^where'?s\s+(?:the\s+)?(.+?)\s+(defined|declared|located|implemented)\??$/i, to: (m) => `where is ${m[1]} ${m[2]}` },
749
- // "were is X defined" (0.9.13 Tier-1 playtest: the missing-h typo of "where").
380
+ // "were is X defined" (the missing-h typo of "where").
750
381
  // NOT curated as a blanket MISSPELLINGS entry — "were" is a real word already
751
382
  // load-bearing as the TEMPORAL_AUX auxiliary ("when were the modules last
752
383
  // touched"), so a global word-boundary rewrite would clobber that reading.
@@ -756,34 +387,16 @@ export const PHRASING_FRAMES = Object.freeze([
756
387
  // a bare "is").
757
388
  { re: /^were\s+is\s+(?:the\s+)?(.+?)\s+(defined|declared|located|implemented)\??$/i, to: (m) => `where is ${m[1]} ${m[2]}` },
758
389
 
759
- // PREDICATIVE QUALIFIER → the ATTRIBUTIVE form the grammar already answers. The
760
- // adjective-qualifier post-filters (ask-vocab.mjs QUALIFIERS: tested/untested,
761
- // public/private, exported, static/abstract/constant, …) parse in the ATTRIBUTIVE
762
- // slot — "untested modules", "public methods" — but a developer just as naturally
763
- // asks the PREDICATIVE "which modules are untested" / "what functions are tested",
764
- // which hit the grammar wall (and, worse, the wall's own hint SUGGESTED "which
765
- // functions are tested" — a shape it could not then answer). Rewriting the
766
- // predicative "<which|what> <kind> are <QUALIFIER>" to "<QUALIFIER> <kind>" routes
767
- // it onto the working attributive filter. Closed to the known qualifier adjectives
768
- // (not a general "… are X" catch), and the QUALIFIER must sit immediately after
769
- // are/is, so "which modules are NOT tested" never matches here — that keeps its own
770
- // set-complement handler (matchNegationSet, downstream in ask.mjs's parseNegation).
390
+ // PREDICATIVE QUALIFIER ("which modules are untested") → the ATTRIBUTIVE form
391
+ // ("untested modules") the grammar already answers. The QUALIFIER must sit
392
+ // immediately after are/is, so "are NOT tested" keeps its own set-complement handler.
771
393
  {
772
394
  re: /^(?:which|what)\s+(?:the\s+|all\s+)?([a-z][a-z-]*?)\s+(?:are|is)\s+(public|private|protected|static|abstract|constant|exported|re-?exported|tested|covered|untested|uncovered)\??$/i,
773
395
  to: (m) => `${m[2].toLowerCase()} ${m[1].toLowerCase()}`,
774
396
  },
775
397
 
776
- // BARE COVERAGE SURVEY (no entity kind) → the attributive "<qualifier> modules"
777
- // the grammar already answers. Once "what is a test" opens the topic, a developer
778
- // asks the survey the plainest way — "what is untested", "what's not tested",
779
- // "what isn't covered", "what is covered" — with NO entity noun at all, so the
780
- // predicative-qualifier frame above (which needs a KIND between what/which and
781
- // are/is) can't catch it, and it fell through to a soft wall ("no module matching
782
- // 'not'…" / the "I answer questions…" orientation). Default the surveyed kind to
783
- // modules (the same set "which modules are not tested" / "untested modules" return)
784
- // and fold the negation into the qualifier (not tested → untested, not covered →
785
- // uncovered). Anchored with no object, so "what tests cover X" / "what is a test"
786
- // never match here.
398
+ // BARE COVERAGE SURVEY, no entity kind ("what is untested") → defaults the
399
+ // surveyed kind to modules and folds the negation into the qualifier.
787
400
  {
788
401
  re: /^what\s+(?:is|are)\s+(not\s+)?(tested|untested|covered|uncovered)\??$/i,
789
402
  to: (m) => {
@@ -793,65 +406,27 @@ export const PHRASING_FRAMES = Object.freeze([
793
406
  },
794
407
  },
795
408
 
796
- // CO-CHANGE → the "co-changes with" canonical the RELATIONS table answers. The
797
- // cochange verb synonyms (ask-vocab.mjs) include "co-changes with" / "moves
798
- // together with" / "tends to change together with", but NOT the plainest form a
799
- // developer types — the one the README itself prints and the relation renders as:
800
- // "what does X change together with" / "what changes together with X". Both hit a
801
- // dead-end ("couldn't resolve one of the terms" / the grammar wall); rewriting them
802
- // onto "what co-changes with X" routes them to the working change-coupling query.
409
+ // CO-CHANGE → "what co-changes with X" (the plainest phrasing a developer types).
803
410
  { re: /^what\s+does\s+(.+?)\s+changes?\s+together\s+with\??$/i, to: (m) => `what co-changes with ${m[1]}` },
804
411
  { re: /^what\s+changes?\s+together\s+with\s+(.+?)\??$/i, to: (m) => `what co-changes with ${m[1]}` },
805
412
 
806
- // AUTHORSHIP → the "who touched X" churn query. "who touched X" now names the
807
- // commit author beside the sha (the 0.8.1 commit-ref quick-win), which invites the
808
- // synonyms a developer reaches for next — "who wrote X", "who authored X", "who is
809
- // the author of X" — and every one of them hit the grammar wall. tmct has no
810
- // separate authorship edge; "touched" IS the authorship signal (the churn commits
811
- // carry the author), so these are true synonyms of "who touched X", not a new
812
- // capability. Anaphora rides through untouched ("who wrote it" → "who touched it").
813
- // SHA GUARD (0.8.2 feel wave): a COMMIT object is NOT a synonym — "who is the
814
- // author of abc1234" rewritten to "who touched abc1234" dumps the commit's
815
- // touch-SET instead of naming its author. The negative lookahead refuses the
816
- // rewrite when the object is a bare (optionally "commit "-prefixed) 7-40 char
817
- // hex sha, leaving the un-rewritten form for the author lane to consume;
818
- // file/symbol objects (anything non-sha, e.g. "deadbeef.mjs") keep the rewrite.
413
+ // AUTHORSHIP → "who touched X" (tmct's touch edge IS the authorship signal).
414
+ // A commit sha object is excluded — that dumps the commit's touch-set, not its author.
819
415
  { re: /^who\s+(?:wrote|authored)\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
820
416
  { re: /^who\s+is\s+the\s+authors?\s+of\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
821
417
 
822
- // HAS-TESTS → the coverage question the RELATIONS table answers. "does X have
823
- // tests" parses "have" as a defines-verb (VERB_TO_KIND), producing the garbled
824
- // "No — no defines edge found from X to <whatever resolves>" receipt; "is X
825
- // tested" traverses tests edges from the WRONG side (subject = X). Both mean
826
- // the coverage question "what tests X" — rewrite onto it. Closed to a
827
- // tests/coverage object ("does X have methods/members" stays the members
828
- // family) and refuses any "not" in the subject span, so the set-complement
829
- // negations ("is X not tested") keep their own handler downstream.
418
+ // HAS-TESTS → "what tests X" (the coverage question). Refuses "not" in the
419
+ // subject so set-complement negations keep their own downstream handler.
830
420
  { re: /^(?:does|do)\s+(?!.*\bnot\b)(.+?)\s+have\s+(?:any\s+)?(?:tests?|test\s+coverage|coverage)\??$/i, to: (m) => `what tests ${m[1]}` },
831
421
  { re: /^(?:is|are)\s+(?!.*\bnot\b)(.+?)\s+tested\??$/i, to: (m) => `what tests ${m[1]}` },
832
422
 
833
- // NEEDS-TESTS → the untested-module survey. "what needs tests" / "what needs
834
- // testing" is the plainest way to ask which modules are uncovered, and it hit the
835
- // grammar wall ("no module matching 'needs'…"). Route it onto the same attributive
836
- // survey the bare "what is untested" frame lands on. Closed to the tests/coverage
837
- // object, so it can't swallow a general "what needs X".
423
+ // NEEDS-TESTS → the untested-module survey.
838
424
  { re: /^what\s+needs\s+(?:to\s+be\s+)?(?:a\s+)?(?:tested|tests?|testing|coverage|covering)\??$/i, to: () => "untested modules" },
839
425
 
840
- // DOES-X-VERB-ANYTHING-ELSE → the plain forward "what does X <verb>" listing
841
- // (0.9.15 Tier-1 single-touch playtest). A very natural drill-down follow-up
842
- // after a relation answer "does listTasks call anything else", "does
843
- // src/handlers/tasks.mjs import something else" used to dead-end: "anything"/
844
- // "something" [else] is a placeholder standing in for "the rest of the list",
845
- // not a real object term, but the two parse strategies disagreed on the SPAN
846
- // (grammar kept "anything else" whole as the object, keyword-spot dropped
847
- // "anything" and kept only "else"), landing on the {ambiguousParse} surface —
848
- // two nonsense readings offered as if one might be right. "what does X <verb>"
849
- // is the exact working canonical shape (see the MEMBERS-of-class frames above),
850
- // so rewriting the whole closed pattern onto it sidesteps the disagreement
851
- // instead of teaching either strategy's tokenizer to special-case "else".
852
- // Anchored to the closed VERB_TO_KIND vocabulary so it can never swallow a
853
- // genuine named object that happens to start with "any"/"some" (only the bare
854
- // placeholder nouns "anything"/"something", optionally trailed by "else", match).
426
+ // DOES-X-VERB-ANYTHING-ELSE → "what does X <verb>" (drops the placeholder
427
+ // "anything/something else" object, which otherwise made the two parse
428
+ // strategies disagree on the span). Anchored to VERB_TO_KIND so it can't
429
+ // swallow a real object that happens to start with "any"/"some".
855
430
  {
856
431
  re: new RegExp(`^(?:do|does)\\s+(.+?)\\s+(${VERB_ALTERNATION})\\s+(?:anything|something)(?:\\s+else)?\\??$`, "i"),
857
432
  to: (m) => `what does ${m[1]} ${m[2]}`,
@@ -870,31 +445,22 @@ export function applyPhrasingFrames(text) {
870
445
  return text;
871
446
  }
872
447
 
873
- // ---- §B1 negation the SET-COMPLEMENT frame (Cycle 5, archive/PLAN_CYCLE_4.md). Recognizes
874
- // a BARE set-negation query — "which X do not <verb> Y", "X that don't <verb> Y",
875
- // "modules not importing Y", "which X are not <qualifier>" — and returns a descriptor
876
- // {entWord, predicate} that ask.mjs's compositional grammar turns into a bounded
877
- // complement (allOfClass(kind) MINUS the positive result set). Deliberately SEPARATE
878
- // from applyNegationFrames/NEGATION_FRAMES above: that table is a rhetorical
879
- // double-negative rewriter that REMOVES negation ("there isn't anything calling it" ->
880
- // "what calls it") and its docblock forbids scope parsing; this detector PRESERVES the
881
- // negation as a set operation. Returns null when no set-negation marker is present, so
882
- // every affirmative query passes through untouched (the active-voice regression guard).
883
- // The entWord is validated against the entity vocabulary by the caller, which also
884
- // enforces the bounded-universe refusal for the non-enumerable "changes" pseudo-type. ----
448
+ // SET-COMPLEMENT frame: recognizes a bare set-negation query ("which X do not
449
+ // <verb> Y") and returns a descriptor, distinct from the rhetorical
450
+ // double-negative rewriter above (which REMOVES negation instead of
451
+ // preserving it as a set operation).
885
452
  const NEGATION_SET_RE = new RegExp(
886
- "^(?:which|what|who|list|show(?:\\s+me)?|find|give\\s+me)?\\s*(?:the\\s+|all\\s+)?" // optional frame + determiner
887
- + "([a-z][a-z-]*)\\s+" // (1) the entity kind noun
888
- + "(?:(?:that|which|who)\\s+)?" // optional relative pronoun
889
- + "(?:(?:do|does|did|are|is|was|were|have|has)\\s+)?" // optional auxiliary
890
- + "not\\s+(.+)$", // the negation marker + (2) the predicate
453
+ "^(?:which|what|who|list|show(?:\\s+me)?|find|give\\s+me)?\\s*(?:the\\s+|all\\s+)?"
454
+ + "([a-z][a-z-]*)\\s+"
455
+ + "(?:(?:that|which|who)\\s+)?"
456
+ + "(?:(?:do|does|did|are|is|was|were|have|has)\\s+)?"
457
+ + "not\\s+(.+)$",
891
458
  "i",
892
459
  );
893
460
 
894
- /** Recognize a bare set-negation query and return {entWord, predicate}, or null when
895
- * no set-negation marker ("not") follows an entity noun. Pure text analysis — the
896
- * caller (ask.mjs's parseNegation) validates the entity kind, refuses the
897
- * non-enumerable "changes" universe, and builds the complement AST. */
461
+ /** Recognize a bare set-negation query -> {entWord, predicate}, or null. The
462
+ * caller (ask.mjs's parseNegation) validates the entity kind and builds the
463
+ * complement AST. */
898
464
  export function matchNegationSet(text) {
899
465
  const m = String(text || "").match(NEGATION_SET_RE);
900
466
  if (!m) return null;
@@ -914,23 +480,9 @@ export const STOPWORDS = new Set([
914
480
  "what", "who", "which", "where", "when", "why", "how",
915
481
  "does", "do", "did", "is", "are", "was", "were", "the", "a", "an", "of", "to", "from", "at", "in", "on",
916
482
  "there", "something", "anything", "nothing", "one", "any",
917
- // temporal filler in when-questions ("when was X last touched") — a symbol
918
- // literally named "last" would be the accepted residual cost, same trade as
919
- // every other stopword.
920
- "last",
921
- // frequency-adverb filler ("what does X usually change together with", "what does
922
- // X typically call") — found live: "usually" glued onto the object term instead of
923
- // being stripped, corrupting resolution ("src/core/store.mjs usually" instead of
924
- // the module alone). Same trade as every other stopword: a symbol literally named
925
- // "usually" would be the accepted residual cost.
926
- "usually", "typically", "generally", "normally", "often", "commonly", "mostly",
927
- // modal auxiliaries ("what SHOULD i look at first") — found live: with no modal in
928
- // this set, "should" reached the cascade's bounded fuzzy-correction step and landed
929
- // within edit distance of the unrelated closed-vocab word "hold" ("defines" synonym,
930
- // ask-vocab.mjs), corrupting the whole query into "what hold i at". Same trade as
931
- // every other stopword: a symbol literally named "should" would be the accepted
932
- // residual cost.
933
- "should", "would", "could", "can", "will", "shall", "might", "must",
483
+ "last", // temporal filler ("when was X last touched")
484
+ "usually", "typically", "generally", "normally", "often", "commonly", "mostly", // frequency-adverb filler
485
+ "should", "would", "could", "can", "will", "shall", "might", "must", // modal auxiliaries
934
486
  ]);
935
487
 
936
488
  /** Split free text into words: trailing "?" run stripped, commas treated as