@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.
- package/README.md +441 -217
- package/bin/tmct.mjs +126 -1
- package/corpus/seon/README.md +1 -2
- package/package.json +4 -2
- package/src/answer-variants.mjs +8 -36
- package/src/ask-browser-entry.mjs +5 -23
- package/src/ask-browser.bundle.js +1 -2
- package/src/ask-nlp.mjs +9 -23
- package/src/ask-vocab.mjs +139 -589
- package/src/ask.mjs +627 -1729
- package/src/chat.mjs +1684 -2874
- package/src/cli-args.mjs +14 -28
- package/src/codegraph.mjs +236 -644
- package/src/completions/complete.mjs +18 -62
- package/src/completions/graph-adapter.mjs +14 -60
- package/src/completions/group.mjs +12 -68
- package/src/completions/infer.mjs +38 -126
- package/src/completions/prune.mjs +17 -70
- package/src/completions/rank.mjs +16 -69
- package/src/completions/search.mjs +8 -31
- package/src/concept.mjs +32 -88
- package/src/conformance.mjs +11 -15
- package/src/corpus/conceptnet.mjs +31 -89
- package/src/corpus/templates.mjs +19 -45
- package/src/corpus/unknown-ingest.mjs +31 -92
- package/src/embed.mjs +10 -22
- package/src/extensions.mjs +50 -154
- package/src/finish.mjs +35 -91
- package/src/grammar/ace.mjs +16 -40
- package/src/grammar/assert.mjs +1 -1
- package/src/grammar/lexicon-core.json +1 -1
- package/src/grammar/lexicon.mjs +9 -27
- package/src/graph-merge.mjs +2 -3
- package/src/hash.mjs +6 -14
- package/src/index.mjs +6 -10
- package/src/init.mjs +38 -125
- package/src/interpret/fuzzy.mjs +10 -29
- package/src/interpret/merge.mjs +9 -27
- package/src/interpret/normalize.mjs +137 -585
- package/src/interpret/pipeline.mjs +23 -71
- package/src/interpret/strategies/ace.mjs +7 -31
- package/src/interpret/strategies/constructions.mjs +14 -41
- package/src/interpret/strategies/grammar.mjs +21 -60
- package/src/interpret/strategies/keywords.mjs +42 -131
- package/src/interpret/strategies/noise-strip.mjs +18 -89
- package/src/memory/bias.mjs +11 -54
- package/src/memory/blocks.mjs +18 -69
- package/src/memory/core.mjs +171 -591
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +7 -25
- package/src/memory/shacl.mjs +10 -39
- package/src/memory/trust.mjs +26 -127
- package/src/memory-ask-browser-entry.mjs +7 -30
- package/src/memory-ask-browser.bundle.js +1 -1
- package/src/paraphrase.mjs +20 -53
- package/src/planning.mjs +15 -157
- package/src/prose-nlp.mjs +4 -17
- package/src/prose.mjs +19 -67
- package/src/providers/bootstrap.mjs +1 -2
- package/src/providers/fixture.mjs +1 -2
- package/src/providers/graph-service.mjs +28 -59
- package/src/repository-interface.mjs +6 -8
- package/src/router/drive.mjs +183 -0
- package/src/router/goal-reasoner.mjs +66 -231
- package/src/router/guardrail.mjs +20 -58
- package/src/router/planner.mjs +15 -46
- package/src/router/registry.mjs +13 -43
- package/src/router/resolver.mjs +46 -131
- package/src/router/results.mjs +231 -0
- package/src/schema-docs.mjs +10 -27
- package/src/server-http.mjs +10 -19
- package/src/server.mjs +22 -28
- package/src/sessions.mjs +15 -30
- package/src/source-slice.mjs +5 -7
- package/src/source.mjs +10 -20
- package/src/syllogise.mjs +187 -575
- package/src/telemetry.mjs +3 -3
- package/src/toml-config.mjs +4 -4
- package/src/tui/app.mjs +9 -19
- package/src/viz.mjs +66 -123
- package/src/wink-model.mjs +10 -24
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
// interpret/normalize.mjs — the input-normalization pass
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
// ----
|
|
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
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// words
|
|
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
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
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"
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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"
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
-
//
|
|
134
|
-
//
|
|
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
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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>".
|
|
238
|
-
* remainder are
|
|
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
|
|
273
|
-
*
|
|
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
|
-
/**
|
|
284
|
-
*
|
|
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
|
-
/**
|
|
300
|
-
*
|
|
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
|
-
/**
|
|
318
|
-
*
|
|
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"
|
|
328
|
-
*
|
|
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>
|
|
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
|
|
341
|
-
*
|
|
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>"
|
|
352
|
-
*
|
|
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:
|
|
362
|
-
* a
|
|
363
|
-
*
|
|
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"
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
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
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
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
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
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>".
|
|
506
|
-
*
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
576
|
-
*
|
|
577
|
-
*
|
|
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
|
|
610
|
-
*
|
|
611
|
-
*
|
|
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
|
|
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
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
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
|
|
665
|
-
//
|
|
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
|
-
/**
|
|
690
|
-
*
|
|
691
|
-
*
|
|
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
|
-
//
|
|
705
|
-
//
|
|
706
|
-
//
|
|
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
|
-
//
|
|
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
|
|
745
|
-
//
|
|
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" (
|
|
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
|
|
760
|
-
//
|
|
761
|
-
//
|
|
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
|
|
777
|
-
//
|
|
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 →
|
|
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 →
|
|
807
|
-
// commit
|
|
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 →
|
|
823
|
-
//
|
|
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.
|
|
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 →
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
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
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
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+)?"
|
|
887
|
-
+ "([a-z][a-z-]*)\\s+"
|
|
888
|
-
+ "(?:(?:that|which|who)\\s+)?"
|
|
889
|
-
+ "(?:(?:do|does|did|are|is|was|were|have|has)\\s+)?"
|
|
890
|
-
+ "not\\s+(.+)$",
|
|
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
|
|
895
|
-
*
|
|
896
|
-
*
|
|
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
|
|
918
|
-
|
|
919
|
-
//
|
|
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
|