@polycode-projects/the-mechanical-code-talker 1.0.8 → 1.0.9

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/src/codegraph.mjs CHANGED
@@ -1408,7 +1408,7 @@ export function renderUntested(graph) {
1408
1408
  return `${untested.length} source module(s) with no covering test module:\n ${capJoin(untested, COVERAGE_CAP, "\n ")}`;
1409
1409
  }
1410
1410
 
1411
- const HISTORY_CAP = 15;
1411
+ export const HISTORY_CAP = 15;
1412
1412
 
1413
1413
  /** Recent commits that touched a symbol's module — from `touches` (seon:history).
1414
1414
  * Replaces `git log -- <file>`. Commits are listed newest-first (git-log order). */
@@ -146,7 +146,19 @@ const isListingRemainder = (rest) => {
146
146
  // splice — "." joins the delimiter class for exactly this reason; a bare
147
147
  // greeting alone ("hi.") still can't match since the regex also requires a
148
148
  // non-empty remainder AFTER the delimiter.
149
- const GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy|g'?day|good\s+(?:morning|afternoon|evening|day)|greetings|salutations)(?:\s+there)?\s*[,.—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
149
+ // "yeah nah" (AU/NZ informal opener, §3b, Tier 6 playtest): a soft discourse
150
+ // filler AU/NZ speakers lead a sentence with, distinct from an actual "no" —
151
+ // chat.mjs's own GREET closed set already recognizes the BARE phrase, but a
152
+ // fused lead-in ("yeah nah, what does the router do") had no preamble form at
153
+ // all and fell straight to the raw grammar wall, the exact failure g'day's own
154
+ // fix (just above) closed for that dialect's greeting word.
155
+ // "howdy pardner" (Tier 6 playtest, §3b dialect axis): "howdy" alone already
156
+ // matched, but a VOCATIVE word right after it ("pardner", a US-Western/cowboy
157
+ // register touch) sat between the greeting and its delimiter, where only the
158
+ // literal word "there" was tolerated — "howdy pardner, remember that X" fell
159
+ // straight to the raw grammar wall. A small closed vocative set, same
160
+ // discipline as the greeting word list itself.
161
+ 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;
150
162
  /** Thanks lead-in with a delimiter (+ optional "quick question" bridge), the
151
163
  * sibling of GREETING_PREAMBLE_RE for the "thanks" word family (Bug B2, 0.8.2
152
164
  * follow-up): "thanks, <Q>" / "thanks so much, <Q>" -> "<Q>". chat.mjs's
@@ -158,6 +170,45 @@ const GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy|g'?day|good\s+(?:mo
158
170
  * non-empty-remainder-REQUIRED discipline as the greeting frame, so a bare
159
171
  * "thanks so much" (no delimiter, no question) stays small-talk. */
160
172
  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;
173
+ /** An ACKNOWLEDGEMENT lead-in with a delimiter, the sibling of GREETING_PREAMBLE_RE/
174
+ * THANKS_PREAMBLE_RE for the "ok"/"cool" word family (Tier 6 playtest: "ok cool,
175
+ * what about the TaskController" — a drill-down continuation politely
176
+ * acknowledging the PREVIOUS answer before asking the next question). chat.mjs's
177
+ * OK_ACK set already treats a BARE "ok"/"cool"/"sounds good" as small-talk, but a
178
+ * CHAINED lead-in ("ok cool, X") left "cool"/"," debris ahead of the real
179
+ * question — same failure class GREETING/THANKS' own multi-word lead-ins had.
180
+ * The marker group repeats (`+`) so a stack of ack-words peels in one pass ("ok
181
+ * cool" both go), each followed by whitespace/comma; same delimiter- and
182
+ * non-empty-remainder-REQUIRED discipline as the two frames above. */
183
+ // "no worries" (Tier 6 playtest, §3b dialect axis, AU/NZ): a casual "that's
184
+ // fine"/"no problem" opener that also, like the other ack words, sometimes
185
+ // leads straight into the NEXT question rather than standing alone. "aight"
186
+ // (cycle 5, §3b typo/elongation axis): the further-dropped texting-register
187
+ // contraction of "alright", chainable with the others just like "ok cool,".
188
+ 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;
189
+ /** A SELF-ORIENTATION lead-in with a delimiter — "just poking around, what's
190
+ * in this repo", "just browsing, X", "just exploring, X" (Tier 6 playtest,
191
+ * §3: the vague-opener family a genuine first-time stranger types). Same
192
+ * delimiter-required discipline as GREETING/THANKS/ACK_PREAMBLE_RE above —
193
+ * a bare "just poking around" with no question stays small-talk (this file
194
+ * never claims a turn that has no remainder to hand back). */
195
+ const BROWSING_PREAMBLE_RE = /^just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)\s*[,.—–-]\s*(.+)$/i;
196
+ /** A repeated leading HEDGE ADVERB ("maybe", "possibly", "perhaps") ahead of a
197
+ * polite request verb — the sibling of ACK_PREAMBLE_RE for HEDGING rather than
198
+ * acknowledging (Tier 6 playtest §3's own stacked-politeness example: "could
199
+ * you maybe possibly tell me... what saveStore does"). Unlike ACK_PREAMBLE_RE,
200
+ * no delimiter is required — a hedge adverb modifies the verb it precedes
201
+ * directly ("maybe possibly tell me"), so requiring a comma would miss the
202
+ * common case; unconditional strip, same as the other preamble frames (none
203
+ * of these three words is grammar-owned vocabulary). Deliberately a narrow,
204
+ * closed three-word set. */
205
+ const HEDGE_ADVERB_PREAMBLE_RE = /^(?:(?:maybe|possibly|perhaps)\s+)+(.+)$/i;
206
+ /** A floating "if it's not too much trouble"/"if that's not too much bother"
207
+ * aside — a genuine mid-sentence PARENTHETICAL (unlike every other frame
208
+ * here, this is NOT anchored to the start of the string), stripped wherever
209
+ * it appears, comma-bounded on either side. Tier 6 playtest's own example
210
+ * phrase: "...tell me, if its not too much trouble, what saveStore does". */
211
+ 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;
161
212
  /** Modal politeness wrapper: "can/could/would/will you [please] <Q>[, please][?]"
162
213
  * -> "<Q>". FILLER_WORDS already ate "can you"/"please" as words; this frame
163
214
  * removes them as a WRAPPER so the ", please" comma never survives into the
@@ -176,6 +227,16 @@ const MODAL_WRAPPER_RE = /^(?:can|could|would|will)\s+you\s+(?:please\s+)?(.+?)(
176
227
  * "explain" elaboration request (WHY set) are matched on the RAW turn text
177
228
  * before normalizeQuery ever runs, so neither is touched by this frame. */
178
229
  const EXPLAIN_WRAPPER_RE = /^explain\s+(?:to\s+me\s+|please\s+)*(.+?)\??$/i;
230
+ /** "tell me <Q>" (bare, no "about") -> "<Q>" — the sibling of EXPLAIN_WRAPPER_RE
231
+ * for the "tell me" verb family when what follows is already a full WH-question
232
+ * rather than a bare noun. "tell me about X" is vagueTouchTermOf's own separate
233
+ * territory (chat.mjs) and is untouched here: this frame's interrogative-lead
234
+ * gate can only ever fire on a remainder that "about X" never satisfies (it
235
+ * starts with "about", not a WH-word). Found live (Tier 6 playtest): "could you
236
+ * maybe possibly tell me... what saveStore does" left "tell me what saveStore
237
+ * does" needing one more unwrap after HEDGE_ADVERB_PREAMBLE_RE and
238
+ * MODAL_WRAPPER_RE peeled their own layers. */
239
+ const TELL_ME_WRAPPER_RE = /^tell\s+me\s+(.+?)\??$/i;
179
240
  /** show/give-me presentation bridge: "show me [the] <thing>". Three-way:
180
241
  * a KIND-listing remainder is left untouched (the compositional list grammar
181
242
  * owns "show me untested modules"); a remainder carrying a relation verb or an
@@ -204,21 +265,63 @@ const SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
204
265
  const LEADING_CONNECTIVE_RE = /^(?:and|also|so|then|now|but)\s+(.+)$/i;
205
266
  const QUESTION_AUX_LEAD_RE = /^(?:does|do|did|is|are|was|were|has|have|had|can|could|will|would|should)\b/i;
206
267
 
268
+ /** A TOPIC-SWITCH / self-interruption preamble — "actually never mind, what
269
+ * calls X", "no wait, I meant what calls Y", "hold on, where is Z defined"
270
+ * (Tier 6 playtest, §3: "no wait", mid-conversation topic switches). A real
271
+ * user abandoning whatever they were about to say and asking something else
272
+ * instead — same species as LEADING_CONNECTIVE_RE just above (a discourse
273
+ * marker carrying no query content of its own, gated on a real question
274
+ * following it), just a richer closed marker set than a single connective
275
+ * word, and CHAINABLE ("actually" + "never mind" + "i meant" can all stack —
276
+ * the fixpoint loop below peels one per pass). Deliberately NOT the same
277
+ * mechanism as SELF_CORRECTION_RE (this file, further down): that shape
278
+ * requires an explicit "sorry"/"i mean" marker WITH a mandatory trailing
279
+ * delimiter, modeling a mid-sentence restart of the SAME clause with real
280
+ * text on both sides; this one is a STANDALONE marker at the very start of
281
+ * the turn with nothing meaningful before it, and the delimiter (a comma) is
282
+ * optional — colloquial speech routinely drops it ("no wait i meant …"). The
283
+ * marker group REPEATS (`+`, mirroring ACK_PREAMBLE_RE just above) so a stack
284
+ * of markers peels in ONE pass ("actually" + "never mind," both go) — unlike
285
+ * LEADING_CONNECTIVE_RE's single-word frame, gating on the remainder after
286
+ * only the FIRST marker would reject the strip before later markers ever get
287
+ * a chance to peel (found live: "actually never mind, X" left "never mind,
288
+ * X" as the gated remainder, which itself never looks like a question lead,
289
+ * so the whole frame silently declined). None of these markers is grammar-
290
+ * owned vocabulary (VERB_TO_KIND/ENTITY_TO_TYPE), so — same as GREETING_
291
+ * PREAMBLE_RE/THANKS_PREAMBLE_RE/ACK_PREAMBLE_RE above — the strip is
292
+ * unconditional; no interrogative-lead gate needed. */
293
+ const TOPIC_SWITCH_PREAMBLE_RE =
294
+ /^(?:(?: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;
295
+
207
296
  /** Apply the closed preamble frames in order (greeting -> modal -> show/give-me),
208
297
  * repeated to a small fixpoint so stacked wrappers ("hey, can you show me X
209
298
  * please") peel fully. Pure and idempotent; unmatched text passes through. */
210
299
  export function applyPreambleFrames(text) {
211
300
  let q = String(text || "");
301
+ // The trouble-aside is a mid-sentence parenthetical, not a start-anchored
302
+ // wrapper — stripped unconditionally, once, before the fixpoint loop (it
303
+ // never interacts with the other frames' anchoring).
304
+ q = q.replace(TROUBLE_ASIDE_RE, " ").replace(/\s+/g, " ").trim();
212
305
  for (let pass = 0; pass < 3; pass++) {
213
306
  const before = q;
214
307
  let m = q.match(GREETING_PREAMBLE_RE);
215
308
  if (m) q = m[1].trim();
216
309
  m = q.match(THANKS_PREAMBLE_RE);
217
310
  if (m) q = m[1].trim();
311
+ m = q.match(ACK_PREAMBLE_RE);
312
+ if (m) q = m[1].trim();
313
+ m = q.match(BROWSING_PREAMBLE_RE);
314
+ if (m) q = m[1].trim();
315
+ m = q.match(HEDGE_ADVERB_PREAMBLE_RE);
316
+ if (m) q = m[1].trim();
317
+ m = q.match(TOPIC_SWITCH_PREAMBLE_RE);
318
+ if (m) q = m[1].trim();
218
319
  m = q.match(MODAL_WRAPPER_RE);
219
320
  if (m) q = m[1].trim();
220
321
  m = q.match(EXPLAIN_WRAPPER_RE);
221
322
  if (m && INTERROGATIVE_LEAD_RE.test(m[1].trim())) q = m[1].trim();
323
+ m = q.match(TELL_ME_WRAPPER_RE);
324
+ if (m && INTERROGATIVE_LEAD_RE.test(m[1].trim())) q = m[1].trim();
222
325
  m = q.match(SHOW_GIVE_ME_RE);
223
326
  if (m) {
224
327
  const rest = m[1].trim();
@@ -229,7 +332,22 @@ export function applyPreambleFrames(text) {
229
332
  m = q.match(LEADING_CONNECTIVE_RE);
230
333
  if (m) {
231
334
  const rest = m[1].trim();
232
- if (INTERROGATIVE_LEAD_RE.test(rest) || QUESTION_AUX_LEAD_RE.test(rest)) q = rest;
335
+ // Tier 6 playtest cycle 5: a leading connective ("so") sandwiched
336
+ // BETWEEN two other discourse markers ("aight cool, so actually wait,
337
+ // what calls X") used to block the WHOLE fixpoint — the remainder right
338
+ // after "so" ("actually wait, X") isn't ITSELF a question yet (it still
339
+ // has its own marker prefix), so the original interrogative-only gate
340
+ // rejected the strip, and TOPIC_SWITCH_PREAMBLE_RE (anchored to start)
341
+ // never got a chance at "actually" while "so " still sat in front of
342
+ // it. Also accepting a remainder that matches one of this file's OWN
343
+ // other closed preamble frames is safe by the same logic those frames
344
+ // already rely on: each is anchored + closed-vocabulary, so a match
345
+ // here guarantees the NEXT pass strips it too, not a guess.
346
+ if (
347
+ INTERROGATIVE_LEAD_RE.test(rest) || QUESTION_AUX_LEAD_RE.test(rest)
348
+ || TOPIC_SWITCH_PREAMBLE_RE.test(rest) || ACK_PREAMBLE_RE.test(rest)
349
+ || HEDGE_ADVERB_PREAMBLE_RE.test(rest) || BROWSING_PREAMBLE_RE.test(rest)
350
+ ) q = rest;
233
351
  }
234
352
  if (q === before) break;
235
353
  }
@@ -298,7 +416,23 @@ export function applySubordinationFrames(text) {
298
416
  // NOT match on a bare dash alone with no marker word: an ordinary em-dash aside
299
417
  // ("modules — like Base — that inherit from X") is common prose, not a restart, and
300
418
  // treating every dash as a delimiter would be a guess this file's discipline forbids
301
- // everywhere else. ----
419
+ // everywhere else.
420
+ //
421
+ // Tier 6 playtest tried making the trailing delimiter optional too (to catch
422
+ // "what calls listTasks -- oh wait, i mean createTask" — no comma after "i
423
+ // mean") and reverted it live: this regex's `.+?` prefix discards EVERYTHING
424
+ // before the marker, which is correct for a FULL-CLAUSE restart (the remainder
425
+ // is a complete new question, verb included, e.g. "which classes inherit from
426
+ // Base") but wrong for an OBJECT-ONLY restart, where the verb clause ("what
427
+ // calls") must survive and only the object swaps. Without the delimiter, this
428
+ // object-only shape reduced to the bare noun "createTask" alone (no verb at
429
+ // all) — a genuine regression from the honest "did you mean listTasks or
430
+ // createTask?" ambiguity nudge the UNCHANGED regex already gives (a real
431
+ // candidate list including the correct answer is an acceptable FLOW under
432
+ // SKILL_CHAT_PLAYTEST.md §0/§2, not a dead end) to a hard wall. Fixing the
433
+ // object-only case properly needs verb-clause-preserving logic this closed,
434
+ // delimiter-anchored frame mechanism isn't shaped for — left as a genuine,
435
+ // narrower ceiling rather than risk widening this proven, tested regex. ----
302
436
  const SELF_CORRECTION_RE =
303
437
  /^.+?(?:\s*(?:--|—|-)\s*)?\b(?:sorry|i\s+mean)\b\s*(?:--|—|-|,|:)\s*(.+)$/i;
304
438
 
@@ -7,6 +7,7 @@
7
7
  import {
8
8
  VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
9
9
  META_MEANING_VERBS, WHERE_MARKERS, MENTION_MARKERS,
10
+ INHERITS_REVERSE_VERBS, stripTrailingScopeFiller,
10
11
  } from "../../ask-vocab.mjs";
11
12
  import { escapeRegex } from "../normalize.mjs";
12
13
 
@@ -20,13 +21,28 @@ const TEMPLATES = [
20
21
  // does/is/do/did, which the reverse/forward templates below never match (those start with
21
22
  // which/what), so precedence between T1 and the rest is structural, not a tie-break guess.
22
23
  // "did" joins does/do for the past-tense commit forms ("did commit <sha> touch X").
24
+ // REVERSE VERB SWAP (Seonix Batch 2 Fix 2): this template fixes subject/object by regex
25
+ // capture POSITION, not by the verb's semantic direction — fine for every forward verb
26
+ // ("subclass of", "imports", …), but "is X a superclass of Y" MEANS the reverse of "is X
27
+ // a subclass of Y" (Y inherits from X, not X from Y). INHERITS_REVERSE_VERBS (ask-vocab.mjs)
28
+ // is the closed set of such reverse phrasings; when the matched verb is one of them,
29
+ // subject/object are swapped here, once, at parse time — so downstream evaluation (ask.mjs)
30
+ // sees "is Y a subclass of X" and needs zero changes of its own. (In practice this exact
31
+ // regex only ever matches a does/do/did lead, so "is …" phrasings actually reach the "ask"
32
+ // shape via keywords.mjs's decomposition strategy instead — that strategy applies the same
33
+ // swap for the same reason; this branch is kept for any does/do/did-led phrasing that names
34
+ // a reverse verb, and for structural symmetry with that sibling strategy.)
23
35
  {
24
36
  name: "ask",
25
37
  re: new RegExp(`^(?:does|do|did)\\s+(.+?)\\s+(${VERB_ALT})\\s+(.+?)\\??$`, "i"),
26
- build: (m) => ({
27
- shape: "ask", entityType: null, modifier: "direct",
28
- kind: VERB_TO_KIND[m[2].toLowerCase()], subject: m[1].trim(), object: m[3].trim(),
29
- }),
38
+ build: (m) => {
39
+ const verb = m[2].toLowerCase();
40
+ const kind = VERB_TO_KIND[verb];
41
+ let subject = m[1].trim();
42
+ let object = m[3].trim();
43
+ if (INHERITS_REVERSE_VERBS.includes(verb)) [subject, object] = [object, subject];
44
+ return { shape: "ask", entityType: null, modifier: "direct", kind, subject, object };
45
+ },
30
46
  },
31
47
  // T2 reverse: "which <entity> [<modifier>] <verb> <object>" — the operator's own example shape.
32
48
  {
@@ -63,15 +79,32 @@ const TEMPLATES = [
63
79
  build: (m) => ({ shape: "meta", entityType: null, modifier: "direct", kind: "meta", object: m[1].trim() }),
64
80
  },
65
81
  // T5 meta: "what is a/an <term>" — the OTHER worked phrasing ("what is a Commit").
66
- // The indefinite article is REQUIRED (not optional): a bare "what is <anything>"
67
- // would also swallow "what is the meaning of this codebase" (an existing, deliberately
68
- // honest grammar-miss regression case — ask.test.mjs/ask-dual-strategy.test.mjs both
69
- // assert it stays null), which never mentions "a"/"an" before its tail. Requiring the
70
- // article keeps this template's reach to the one worked shape without reopening that.
82
+ // Seonix Batch 2 Fix 1: the indefinite article is now OPTIONAL, but the BARE
83
+ // (no-article) form is restricted to the CLOSED vocabulary ENTITY_TO_TYPE already
84
+ // imported above (function/method/class/module/attribute/variable/change/commit,
85
+ // singular and plural) — build() returns null (same "this template didn't actually
86
+ // match, keep scanning" contract T8/"when" below already relies on see
87
+ // parseAnchored's own docblock) when the bare form's object isn't one of those
88
+ // closed terms, so the scan falls through exactly as if this template had not
89
+ // matched at all. This keeps "what is a doohickey"/"widget"/"gizmo" (still routed
90
+ // here via the WITH-article, fully unrestricted `(.+?)` path — pinned to resolve as
91
+ // an honest meta miss downstream, ask-combo.test.mjs/chat-readback.test.mjs) working
92
+ // unmodified, while ALSO keeping the two pinned bare-form honest misses intact:
93
+ // "what is the meaning of this codebase" (ask.test.mjs/ask-dual-strategy.test.mjs)
94
+ // and "what is exposed" (ask.test.mjs:840) both have bare objects absent from
95
+ // ENTITY_TO_TYPE, so build() still rejects them and they still fall through to null.
96
+ // Fix 3: stripTrailingScopeFiller (ask-vocab.mjs) trims a curated trailing clause
97
+ // ("what is a Module in this graph" -> "Module") off the object before it's
98
+ // returned, so a scoping tail never corrupts the lookup term either the bare-form
99
+ // check above or downstream resolution/rendering perform.
71
100
  {
72
101
  name: "meta-whatis",
73
- re: new RegExp(`^what\\s+(?:is|are)\\s+(?:an?)\\s+(.+?)\\??$`, "i"),
74
- build: (m) => ({ shape: "meta", entityType: null, modifier: "direct", kind: "meta", object: m[1].trim() }),
102
+ re: new RegExp(`^what\\s+(?:is|are)\\s+(?:(an?)\\s+)?(.+?)\\??$`, "i"),
103
+ build: (m) => {
104
+ const object = m[2].trim();
105
+ if (!m[1] && !ENTITY_TO_TYPE[object.toLowerCase()]) return null; // bare form: closed-set only
106
+ return { shape: "meta", entityType: null, modifier: "direct", kind: "meta", object: stripTrailingScopeFiller(object) };
107
+ },
75
108
  },
76
109
  // T6 mention: "where is <term> mentioned/referenced" — the prose/mentions surface
77
110
  // (2026-07-02 query families). Tried BEFORE T7: T7's trailing marker is optional,
@@ -8,6 +8,7 @@
8
8
  import {
9
9
  VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
10
10
  WHERE_MARKERS, MENTION_MARKERS, PLACEHOLDER_NOUNS, PASSIVE_PARTICIPLE_TO_KIND,
11
+ INHERITS_REVERSE_VERBS,
11
12
  } from "../../ask-vocab.mjs";
12
13
  import { STOPWORDS } from "../normalize.mjs";
13
14
  import { VOCAB_WORDS, eligibleForCanon, fuzzyVocabWord } from "../fuzzy.mjs";
@@ -227,7 +228,20 @@ export function parseKeywordSpot(text, nlp = null) {
227
228
  }
228
229
  }
229
230
 
230
- if (beforeText && afterText) return { shape: "ask", entityType: null, modifier: "direct", kind, subject: beforeText, object: afterText };
231
+ if (beforeText && afterText) {
232
+ // REVERSE VERB SWAP (Seonix Batch 2 Fix 2): this decomposition assigns subject/
233
+ // object by POSITION (before the verb / after it), not by the verb's semantic
234
+ // direction — the same structural fact grammar.mjs's T1 "ask" template comment
235
+ // explains. "is X a superclass of Y" (or its bare "superclass" stem — see
236
+ // INHERITS_REVERSE_VERBS's own comment in ask-vocab.mjs) means the REVERSE of
237
+ // "is X a subclass of Y": swap once, here, at parse time, so evaluation always
238
+ // sees the equivalent forward-phrased question.
239
+ const verbPhrase = canonWords.slice(verbHit.start, verbHit.end).join(" ");
240
+ let subject = beforeText;
241
+ let object = afterText;
242
+ if (INHERITS_REVERSE_VERBS.includes(verbPhrase)) [subject, object] = [object, subject];
243
+ return { shape: "ask", entityType: null, modifier: "direct", kind, subject, object };
244
+ }
231
245
  if (afterText) return { shape: "reverse", entityType, modifier, kind, object: afterText };
232
246
  // forward keeps the spotted entityType ("which modules did commit <sha> touch" is a
233
247
  // forward decomposition — subject before the verb — whose asked grain would otherwise
@@ -424,12 +424,26 @@ export async function appendUtterances(dir, utterances) {
424
424
  * spelling and the graph stays queryable: ConceptNet's /c/en/foo_bar, a
425
425
  * grammar's tmct:Foo_bar and a bare "Foo bar" all become "foo bar". The
426
426
  * PREDICATE is deliberately NOT normalized this way - it is a controlled
427
- * vocabulary term (rdfs:subClassOf) whose casing is meaningful. */
427
+ * vocabulary term (rdfs:subClassOf) whose casing is meaningful.
428
+ *
429
+ * Tier-5 playtest fix (2026-07-09): also strips a leading "the"/"a"/"an" —
430
+ * found live via "remember that THE logger module is deprecated" (teach-side
431
+ * already stripped it before this ran, so storage was unaffected) followed by
432
+ * "what do you know about THE logger module" / "who maintains THE tasks
433
+ * handler" (recall-side queries, which do NOT pre-strip their own captured
434
+ * term before calling this) genuinely missing the just-taught fact — every
435
+ * recall regex in chat.mjs (KNOW_ABOUT_RE, WHO_OWNS_RE, ISA_ASK_RE, …) calls
436
+ * factTermVariants -> normFactTerm on the raw captured term, so fixing it
437
+ * ONCE here closes the gap for all of them instead of patching each site.
438
+ * Safe for storage too (idempotent — an already-stripped subject is
439
+ * unaffected); the article is a determiner, never semantically distinguishing
440
+ * for a code-entity or common-noun term in this domain. */
428
441
  export function normFactTerm(t) {
429
442
  let s = normText(t);
430
443
  s = s.replace(/^\/c\/[a-z]{2,3}\//i, "");
431
444
  s = s.replace(/^[a-z][\w.-]*:/i, "");
432
445
  s = s.replace(/_/g, " ").replace(/\s+/g, " ").trim();
446
+ s = s.replace(/^(?:the|an?)\s+/i, "");
433
447
  return s.toLowerCase();
434
448
  }
435
449
 
package/src/syllogise.mjs CHANGED
Binary file