@polycode-projects/the-mechanical-code-talker 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +441 -202
  2. package/bin/tmct.mjs +126 -1
  3. package/package.json +4 -2
  4. package/src/answer-variants.mjs +8 -36
  5. package/src/ask-browser-entry.mjs +5 -23
  6. package/src/ask-browser.bundle.js +1 -2
  7. package/src/ask-nlp.mjs +9 -23
  8. package/src/ask-vocab.mjs +139 -589
  9. package/src/ask.mjs +627 -1729
  10. package/src/chat.mjs +1684 -2872
  11. package/src/cli-args.mjs +14 -28
  12. package/src/codegraph.mjs +236 -644
  13. package/src/completions/complete.mjs +18 -62
  14. package/src/completions/graph-adapter.mjs +14 -60
  15. package/src/completions/group.mjs +12 -68
  16. package/src/completions/infer.mjs +38 -126
  17. package/src/completions/prune.mjs +17 -70
  18. package/src/completions/rank.mjs +16 -69
  19. package/src/completions/search.mjs +8 -31
  20. package/src/concept.mjs +32 -88
  21. package/src/conformance.mjs +11 -15
  22. package/src/corpus/conceptnet.mjs +31 -89
  23. package/src/corpus/templates.mjs +19 -45
  24. package/src/corpus/unknown-ingest.mjs +31 -92
  25. package/src/embed.mjs +10 -22
  26. package/src/extensions.mjs +50 -154
  27. package/src/finish.mjs +35 -91
  28. package/src/grammar/ace.mjs +16 -40
  29. package/src/grammar/assert.mjs +1 -1
  30. package/src/grammar/lexicon-core.json +1 -1
  31. package/src/grammar/lexicon.mjs +9 -27
  32. package/src/graph-merge.mjs +2 -3
  33. package/src/hash.mjs +6 -14
  34. package/src/index.mjs +6 -10
  35. package/src/init.mjs +38 -125
  36. package/src/interpret/fuzzy.mjs +10 -29
  37. package/src/interpret/merge.mjs +9 -27
  38. package/src/interpret/normalize.mjs +137 -585
  39. package/src/interpret/pipeline.mjs +23 -71
  40. package/src/interpret/strategies/ace.mjs +7 -31
  41. package/src/interpret/strategies/constructions.mjs +14 -41
  42. package/src/interpret/strategies/grammar.mjs +21 -60
  43. package/src/interpret/strategies/keywords.mjs +42 -131
  44. package/src/interpret/strategies/noise-strip.mjs +18 -89
  45. package/src/memory/bias.mjs +11 -54
  46. package/src/memory/blocks.mjs +18 -69
  47. package/src/memory/core.mjs +171 -591
  48. package/src/memory/fold.mjs +0 -0
  49. package/src/memory/inspect.mjs +7 -25
  50. package/src/memory/shacl.mjs +10 -39
  51. package/src/memory/trust.mjs +26 -127
  52. package/src/memory-ask-browser-entry.mjs +7 -30
  53. package/src/memory-ask-browser.bundle.js +1 -1
  54. package/src/paraphrase.mjs +20 -53
  55. package/src/planning.mjs +15 -157
  56. package/src/prose-nlp.mjs +4 -17
  57. package/src/prose.mjs +19 -67
  58. package/src/providers/bootstrap.mjs +1 -2
  59. package/src/providers/fixture.mjs +1 -2
  60. package/src/providers/graph-service.mjs +28 -59
  61. package/src/repository-interface.mjs +6 -8
  62. package/src/router/drive.mjs +183 -0
  63. package/src/router/goal-reasoner.mjs +66 -231
  64. package/src/router/guardrail.mjs +20 -58
  65. package/src/router/planner.mjs +15 -46
  66. package/src/router/registry.mjs +13 -43
  67. package/src/router/resolver.mjs +46 -131
  68. package/src/router/results.mjs +231 -0
  69. package/src/schema-docs.mjs +10 -27
  70. package/src/server-http.mjs +10 -19
  71. package/src/server.mjs +22 -28
  72. package/src/sessions.mjs +15 -30
  73. package/src/source-slice.mjs +5 -7
  74. package/src/source.mjs +10 -20
  75. package/src/syllogise.mjs +187 -575
  76. package/src/telemetry.mjs +3 -3
  77. package/src/toml-config.mjs +4 -4
  78. package/src/tui/app.mjs +9 -19
  79. package/src/viz.mjs +66 -123
  80. package/src/wink-model.mjs +10 -24
@@ -1,7 +1,7 @@
1
- // interpret/strategies/keywords.mjs — strategy 2: keyword-spotting/decomposition,
2
- // extracted MOVE-only from ask.mjs (item 13). ELIZA's own mechanism: find the
3
- // keyword(s) anywhere in the text, decompose around them, tolerate reordering and
4
- // casual phrasing. Position-independent (no `^...$` anchor), so it tolerates
1
+ // interpret/strategies/keywords.mjs — strategy 2: keyword-spotting/decomposition.
2
+ // ELIZA's own mechanism: find the keyword(s) anywhere in the text, decompose
3
+ // around them, tolerate reordering and casual phrasing. Position-independent
4
+ // (no `^...$` anchor), so it tolerates
5
5
  // "what calls this" / "who invokes this" / "something executes this, where from"
6
6
  // — real phrasings the anchored grammar's fixed shapes don't cover.
7
7
 
@@ -13,11 +13,9 @@ import {
13
13
  import { STOPWORDS } from "../normalize.mjs";
14
14
  import { VOCAB_WORDS, eligibleForCanon, fuzzyVocabWord } from "../fuzzy.mjs";
15
15
 
16
- // Reversible-passive detection (Cycle 6, archive/PLAN_CYCLE_4.md): the passive auxiliaries that,
17
- // together with an agent-marking "by", flip the active reading, and the wh-words that
18
- // mark a QUESTIONED agent ("by which classes" / stranded "who is X tested by"). Bare
19
- // "do/does/did" are deliberately EXCLUDED — "which X do not <verb> Y" is a NEGATION, not
20
- // a passive, and must be left for the compositional complement frame.
16
+ // Passive auxiliaries (with an agent-marking "by") that flip the active reading,
17
+ // and the wh-words marking a questioned agent. Bare do/does/did are excluded
18
+ // that shape is a negation, not a passive.
21
19
  const PASSIVE_AUX = new Set(["is", "are", "was", "were", "be", "been", "being", "get", "gets", "got"]);
22
20
  const WH_WORDS = new Set(["which", "what", "who", "whom", "whose"]);
23
21
  const PLACEHOLDER_SET = new Set(PLACEHOLDER_NOUNS.map((w) => w.toLowerCase()));
@@ -42,38 +40,13 @@ export function findPhrase(lcWords, table, consumed = null) {
42
40
  return null;
43
41
  }
44
42
 
45
- /** Strategy 2: scan (already-normalized) text for a verb keyword anywhere,
46
- * plus optional entity/modifier keywords anywhere, then split whatever's
47
- * left (after removing the matched spans + stopwords) into the words BEFORE
48
- * and AFTER the verb. Which side(s) are non-empty decides the shape —
49
- * mirrors the three anchored shapes but by decomposition instead of a fixed
50
- * template, so it tolerates reordering/casual phrasing the anchored regexes
51
- * don't: text on BOTH sides ("does X import Y") -> ask{subject:before,
52
- * object:after}; only AFTER the verb ("what calls this") -> reverse{object:
53
- * after}; only BEFORE it ("what does X import") -> forward{object:before}.
54
- * A lone context pronoun ("this"/"it"/"that"/"here") ending up as a resolved
55
- * term is left as plain text — resolveTermOrContext (traverse-time)
56
- * recognizes it against an optional contextId, so no separate flag is
57
- * needed here. A misparse here costs nothing beyond an honest object-miss
58
- * downstream (resolveObject never guesses).
59
- *
60
- * Keyword matching is TIERED (two-level fuzzy work, 2026-07-02) — each lower
61
- * tier fires ONLY when every tier above found no verb phrase at all, so an
62
- * exact curated match can never be displaced:
63
- * 1. exact — the words as typed (post-normalization, which already applied
64
- * the curated CONTRACTIONS/MISSPELLINGS/WRONG_WORDS corrections);
65
- * 2. lemma (only with the optional Node-side `nlp` adapter) — each eligible
66
- * word is replaced by its wink lemma IF that lemma is itself a vocab word
67
- * ("imported"/"importing" -> "import"), so inflections hit the curated
68
- * phrases without enumerating them. Every verb family already stores its
69
- * lemma form ("import", "call", "touch", "use", …), so a direct
70
- * lemma-in-vocab check is the whole lookup — no reverse index needed;
71
- * 3. fuzzy (adapter-free; works in the inlined viewer too) — a word ≥4 chars
72
- * matching nothing exactly may rewrite to a UNIQUE verb/modifier
73
- * constituent within the bounded edit distance (see fuzzyVocabWord; ties
74
- * are refused, entity nouns are never fuzzy targets).
75
- * The canonicalized words drive PHRASE FINDING only — sideText always reads the
76
- * ORIGINAL words, so a correction can never corrupt an object/subject term. */
43
+ /** Strategy 2: find a verb keyword anywhere in the text, plus optional
44
+ * entity/modifier keywords, then split what's left into words BEFORE and
45
+ * AFTER the verb both sides -> ask, after only -> reverse, before only ->
46
+ * forward. Keyword matching is tiered (exact, then lemma, then fuzzy edit-
47
+ * distance), each tier firing only when every tier above found nothing, so
48
+ * an exact curated match can never be displaced. Canonicalized words drive
49
+ * phrase finding only; sideText always reads the original words. */
77
50
  export function parseKeywordSpot(text, nlp = null) {
78
51
  // Strip a trailing "?" (mirrors the anchored templates' own `\??$`) and turn commas into
79
52
  // pauses/spaces — but NEVER strip a mid-word ".": object terms are routinely dotted file/module
@@ -81,12 +54,8 @@ export function parseKeywordSpot(text, nlp = null) {
81
54
  // must too or the two strategies would "disagree" over a period that was never part of the intent.
82
55
  const words = text.replace(/\?+\s*$/, "").replace(/,/g, " ").split(/\s+/).filter(Boolean);
83
56
  const lcWords = words.map((w) => w.toLowerCase());
84
- // where/mentions shapes (2026-07-02 query families): "where is X [defined]" and
85
- // "where is X mentioned" carry NO relation verb, so the verb-driven decomposition
86
- // below can never reach them. Routed here by the "where" question word + marker —
87
- // but ONLY when no relation verb exists anywhere in the sentence: "something
88
- // executes this, where from" (an existing worked phrasing) has a verb, and its
89
- // "where" is decorative, not a location question.
57
+ // where/mentions carry no relation verb, so only routed here when the sentence
58
+ // has no relation verb anywhere (otherwise "where" is merely decorative).
90
59
  if (lcWords.includes("where") && !findPhrase(lcWords, VERB_TO_KIND)) {
91
60
  const mention = lcWords.some((w) => MENTION_MARKERS.includes(w));
92
61
  const markers = new Set([...WHERE_MARKERS, ...MENTION_MARKERS]);
@@ -96,17 +65,10 @@ export function parseKeywordSpot(text, nlp = null) {
96
65
  return { shape: kind, entityType: null, modifier: "direct", kind, object: objText };
97
66
  }
98
67
  }
99
- // has/have/had-changed carve-out (PLAN_CHAT_FEEL item 6 remainder): "has X
100
- // changed" / "have X touched" / "had X ever been updated" the present/past-
101
- // perfect yes/no frame over the SAME touches-family verb the "when" branch below
102
- // already answers ("when did X change"). Routed BEFORE the general verb-priority
103
- // scan: "has"/"have"/"had" is ALSO a curated `defines` verb ("this module HAS
104
- // three functions") and, sitting first in the sentence, would otherwise win that
105
- // scan outright, misreading "has X changed" as a `defines` reverse-query over
106
- // the object "X changed" — a confidently-wrong grain, not an honest miss. Gated
107
- // on the sentence's OWN final word being a genuine touches verb (never a guess
108
- // at which verb the sentence "really" means) — an ordinary defines question
109
- // ("has app.mjs three functions") never ends in one, so this can't shadow it.
68
+ // has/have/had-changed carve-out ("has X changed"): routed before the general
69
+ // verb scan because has/have/had is ALSO the `defines` verb and would otherwise
70
+ // misread this as a defines query; gated on the sentence's own final word being
71
+ // a genuine touches verb.
110
72
  const PERFECT_AUX = new Set(["has", "have", "had"]);
111
73
  if (PERFECT_AUX.has(lcWords[0])) {
112
74
  let end = lcWords.length;
@@ -139,25 +101,17 @@ export function parseKeywordSpot(text, nlp = null) {
139
101
  if (verbHit) canonWords = fuzzyWords;
140
102
  }
141
103
  if (!verbHit && lcWords.includes("by")) {
142
- // passive-participle rescue (Cycle 6): a participle whose kind is NOT a standalone
143
- // active verb here ("defined" belongs to "is defined in"; bare "inherited" has no
144
- // active key) still marks a passive when a passive auxiliary and an agent "by" are
145
- // present. Consulted ONLY on this exact gate (aux + by), so the active grammar and
146
- // the where-marker routing ("where is X defined") are never disturbed.
104
+ // A participle with no active verb entry still marks a passive when a passive
105
+ // auxiliary and an agent "by" are both present.
147
106
  for (let i = 0; i < lcWords.length; i += 1) {
148
107
  const k = PASSIVE_PARTICIPLE_TO_KIND[lcWords[i]];
149
108
  if (k && lcWords.slice(0, i).some((w) => PASSIVE_AUX.has(w))) { verbHit = { kind: k, start: i, end: i + 1 }; break; }
150
109
  }
151
110
  }
152
111
  if (!verbHit) return null;
153
- // POS consumer (wink adapter, Node-side only): rescue the ONE decomposition this
154
- // strategy provably mis-parses a relation word used as a NOUN in a "the
155
- // <imports> of <term>" nominal ("show the imports of walk.mjs" otherwise
156
- // decomposes to ask{subject:"show"}; bare "the imports of walk.mjs" to the
157
- // reverse shape, both wrong). The wink probe showed "import" is tagged NOUN even
158
- // in genuine verb use ("which modules import walk.mjs"), so the POS signal is
159
- // deliberately NOT a general verb veto — it only fires inside this exact
160
- // det+NOUN+"of" frame, where the nominal reading is grammatically forced.
112
+ // POS rescue (Node-side only): a relation word used as a NOUN in a "the
113
+ // <imports> of <term>" frame would otherwise misparse; only fires inside this
114
+ // exact det+NOUN+"of" shape, since the same word tags NOUN in genuine verb use too.
161
115
  if (nlp && verbHit.end - verbHit.start === 1) {
162
116
  const i = verbHit.start;
163
117
  const det = lcWords[i - 1];
@@ -172,15 +126,8 @@ export function parseKeywordSpot(text, nlp = null) {
172
126
  const consumed = new Set();
173
127
  const mark = (hit) => { if (hit) for (let i = hit.start; i < hit.end; i += 1) consumed.add(i); };
174
128
  mark(verbHit);
175
- // A redundant SAME-KIND verb immediately after the matched one ("what TESTS
176
- // cover X" "tests" is read as the relation trigger, but "cover" also a
177
- // `tests` verb, ask-vocab.mjs's own synonym list — is the word the sentence
178
- // actually intends as the verb) would otherwise fall into afterText raw and
179
- // corrupt the object ("cover src/core/model.mjs" instead of the module alone).
180
- // Anchored to the exact position right after verbHit (never a general re-scan),
181
- // and gated on matching the SAME kind, so this can only strip a genuine
182
- // restatement, never eat a real object term. Found live: "what tests cover X",
183
- // "which tests test X" both misparsed this way before this fix.
129
+ // A redundant same-kind verb restating the first ("what TESTS cover X") would
130
+ // otherwise corrupt the object; strip it, anchored right after verbHit.
184
131
  for (const [phrase, kind] of Object.entries(VERB_TO_KIND)) {
185
132
  if (kind !== verbHit.kind) continue;
186
133
  const pWords = phrase.split(" ");
@@ -204,39 +151,24 @@ export function parseKeywordSpot(text, nlp = null) {
204
151
  const entityType = entityHit ? ENTITY_TO_TYPE[canonWords.slice(entityHit.start, entityHit.end).join(" ")] : null;
205
152
  const modifier = modifierHit ? MODIFIER_TO_KIND[canonWords.slice(modifierHit.start, modifierHit.end).join(" ")] : "direct";
206
153
 
207
- // when shape (2026-07-02 query families): "when did X change" / "when was X last
208
- // touched" — the "when" question word turns a touches decomposition temporal.
209
- // Only touches carries commit dates to answer with; a "when" next to any other
210
- // relation verb falls through to the ordinary shapes (and their honest answers).
154
+ // "when" turns a touches decomposition temporal; other verbs fall through.
211
155
  if (kind === "touches" && lcWords.includes("when")) {
212
156
  const objText = beforeText || afterText;
213
157
  if (objText) return { shape: "when", entityType: null, modifier: "direct", kind: "touches", object: objText };
214
158
  }
215
159
 
216
- // who-last shape (HANDOVER.md 2026-07-10 item 5): "who last touched X" / "who
217
- // touched X last" used to fall into the ordinary reverse-list shape below and
218
- // list EVERY touching commit's author, ignoring "last" entirely — the mirror
219
- // gap of the "when" shape just above, which already answers a single newest
220
- // commit. "who"/"last" are both STOPWORDS (normalize.mjs), so they never
221
- // survive into beforeText/afterText either way — checked here, before they're
222
- // stripped, the same way the "when" check above reads `lcWords` directly
223
- // rather than the post-strip text.
160
+ // "who last touched X" would otherwise list every touching commit's author,
161
+ // ignoring "last" checked directly against lcWords since both words are
162
+ // stopwords and wouldn't survive into beforeText/afterText.
224
163
  if (kind === "touches" && lcWords.includes("who") && lcWords.includes("last")) {
225
164
  const objText = beforeText || afterText;
226
165
  if (objText) return { shape: "whoLast", entityType: null, modifier: "direct", kind: "touches", object: objText };
227
166
  }
228
167
 
229
- // reversible passive (Cycle 6, archive/PLAN_CYCLE_4.md): "PATIENT is VERBed BY AGENT" an
230
- // agent-marking "by" plus a passive auxiliary flips the active reading, so the AGENT
231
- // (after "by") is the edge SUBJECT and the PATIENT the edge OBJECT. Object-first
232
- // phrasing is otherwise read subject-first and the edge traversed backwards. Fires
233
- // ONLY on a genuine agent "by": a passive auxiliary before the verb AND a standalone
234
- // "by" NOT already swallowed into a multi-word verb phrase ("touched by"/"modified
235
- // by" are single touches verbs, so their "by" is consumed and never triggers this) —
236
- // an active query whose object merely contains a "by" token is untouched (the
237
- // regression guard). The single NAMED role term becomes the object; whether the AGENT
238
- // is named ("by b.test.mjs" → forward from the agent) or QUESTIONED ("by which
239
- // classes" / a stranded "…tested by" → reverse over the patient) picks the direction.
168
+ // Reversible passive ("PATIENT is VERBed BY AGENT"): a passive auxiliary plus a
169
+ // standalone agent-marking "by" (not already swallowed into a multi-word verb
170
+ // phrase) flips subject/object; whether the agent is named or questioned picks
171
+ // forward vs. reverse.
240
172
  const byIdx = lcWords.indexOf("by");
241
173
  const hasPassiveAux = lcWords.slice(0, verbHit.start).some((w) => PASSIVE_AUX.has(w));
242
174
  if (byIdx >= 0 && !consumed.has(byIdx) && hasPassiveAux) {
@@ -263,13 +195,8 @@ export function parseKeywordSpot(text, nlp = null) {
263
195
  }
264
196
 
265
197
  if (beforeText && afterText) {
266
- // REVERSE VERB SWAP (Seonix Batch 2 Fix 2): this decomposition assigns subject/
267
- // object by POSITION (before the verb / after it), not by the verb's semantic
268
- // direction — the same structural fact grammar.mjs's T1 "ask" template comment
269
- // explains. "is X a superclass of Y" (or its bare "superclass" stem — see
270
- // INHERITS_REVERSE_VERBS's own comment in ask-vocab.mjs) means the REVERSE of
271
- // "is X a subclass of Y": swap once, here, at parse time, so evaluation always
272
- // sees the equivalent forward-phrased question.
198
+ // A semantically-reverse verb ("superclass of") swaps subject/object, same as
199
+ // grammar.mjs's T1.
273
200
  const verbPhrase = canonWords.slice(verbHit.start, verbHit.end).join(" ");
274
201
  let subject = beforeText;
275
202
  let object = afterText;
@@ -277,31 +204,15 @@ export function parseKeywordSpot(text, nlp = null) {
277
204
  return { shape: "ask", entityType: null, modifier: "direct", kind, subject, object };
278
205
  }
279
206
  if (afterText) return { shape: "reverse", entityType, modifier, kind, object: afterText };
280
- // "what is a kind of class" / "what inherits from function" (live-caught 2026-07-11
281
- // follow-up to the ambiguousParse fix, commit 5c858bf): entityHit above is found
282
- // ANYWHERE in the sentence and marked consumed before afterText is computed, so
283
- // when the "kind of X"/"inherits X" idiom's object IS ITSELF one of the four
284
- // code-graph entity-type nouns (class/function/method/module — ENTITY_TO_TYPE's
285
- // own keys), the entity match swallows the ENTIRE post-verb span as a (wrong, in
286
- // this shape) grain qualifier, leaving afterText empty and no candidate at all —
287
- // the query silently fails to parse rather than answering or declining honestly.
288
- // Scoped narrowly to kind==="inherits" (the one relation whose object is routinely
289
- // a bare vocabulary noun with no further qualifier) and only when the ENTIRE
290
- // post-verb span was consumed by the entity match (nothing else remains to be an
291
- // object): re-read that span as the literal OBJECT text instead, entityType null
292
- // (it names the thing being asked about here, not a grain filter on some other
293
- // object). Every other kind/shape is unaffected — this never fires unless
294
- // afterText is otherwise empty AND the sole cause is an entity-consumed span
295
- // immediately after the verb.
207
+ // "what is a kind of class": when the object is itself an entity-type noun, the
208
+ // entity match swallows the whole post-verb span as a grain qualifier, leaving
209
+ // afterText empty re-read that span as the object instead.
296
210
  if (kind === "inherits" && !beforeText && entityHit && entityHit.start === verbHit.end) {
297
211
  const entityText = canonWords.slice(entityHit.start, entityHit.end).join(" ");
298
212
  if (entityText) return { shape: "reverse", entityType: null, modifier, kind, object: entityText };
299
213
  }
300
- // forward keeps the spotted entityType ("which modules did commit <sha> touch" is a
301
- // forward decomposition subject before the verb whose asked grain would otherwise
302
- // be lost); traverse() only consults it for the commit-as-subject grain selection,
303
- // so plain forwards behave exactly as before. Modifier stays hardcoded: no forward
304
- // closure traversal exists (see ask.mjs's modifierIsWired).
214
+ // forward keeps the spotted entityType (traverse()'s commit-as-subject grain
215
+ // selection); modifier stays hardcoded since no forward closure traversal exists.
305
216
  if (beforeText) return { shape: "forward", entityType, modifier: "direct", kind, object: beforeText };
306
217
  return null;
307
218
  }
@@ -1,45 +1,12 @@
1
- // interpret/strategies/noise-strip.mjs — the item-10 noise-tolerant fallback
2
- // strategy: strip filler/noise/stop words the closed grammar gives no meaning to,
3
- // then RE-RUN THE PARSE over what's left (the anchored grammar first, then the
4
- // keyword-spot decomposition — see the discipline notes). Rationale: the keyword-spot
5
- // strategy decomposes around the verb and a leading vocative/adverb lands in the
6
- // SUBJECT slot ("hey man which modules import X" -> ask{subject:"man"}), an
7
- // unresolvable term the relaxation cascade can only rescue when the true answer
8
- // is positive (it refuses to relax into a miss) — so a noise-wrapped question
9
- // whose honest answer is negative/empty dies as "couldn't resolve one of the
10
- // terms". Stripping the noise FIRST recovers the template parse and the same
11
- // honest answer the clean phrasing gets.
1
+ // interpret/strategies/noise-strip.mjs — noise-tolerant fallback strategy:
2
+ // strip filler/noise/stop words the closed grammar gives no meaning to, then
3
+ // re-parse what's left (anchored templates first, then keyword-spot).
12
4
  //
13
- // Discipline (no behavior change to anything that already parses):
14
- // · fires ONLY when the anchored grammar MISSES the text as-given if a
15
- // template already matches, the grammar owns the sentence, noise and all;
16
- // · strips ONLY all-lowercase alphabetic tokens (a Capitalized/dotted/digit
17
- // token names something) that are curated noise (FILLER_WORDS + the
18
- // cascade's CASCADE_NOISE) or wink-flagged English stop words (ctx.nlp's
19
- // isStopWord — the optional Node-only tier), and NEVER a word in KEEP: the
20
- // grammar's own vocabulary, question scaffolding, and context pronouns;
21
- // · returns a candidate ONLY when the stripped text then parses — first against
22
- // the anchored TEMPLATES (the strictest parser), then (cycle-2 robustness,
23
- // CHATBENCH_001 L1) against the keyword-spot decomposition over the SAME
24
- // stripped text. The second tier exists because the clean phrasing of half
25
- // the worked questions ("what calls fnAlpha") is itself a keyword-spot
26
- // parse, not a template — so a noise-wrapped variant ("i was wondering what
27
- // calls fnAlpha", "hey tmct, what calls fnAlpha thanks") could never be
28
- // rescued by the template re-parse alone and died as "couldn't resolve one
29
- // of the terms". The keyword-spot re-parse also swallows auxiliary-verb
30
- // residue for free ("was what calls fnAlpha" → reverse{fnAlpha}): its
31
- // decomposition filters STOPWORDS out of the subject/object sides, which is
32
- // exactly where a stripped frame's "was"/"is" residue lands. Same cost
33
- // bound as before: only curated-noise/stop-word tokens were removed, and a
34
- // wrongly-stripped word can at worst cost an honest object-miss downstream
35
- // (resolveObject never guesses), never manufacture an entity.
36
- //
37
- // Registered with its own class ("noise-stripped") at confidence 0.75: above
38
- // keyword-spot (0.7 — over noisy text its decomposition has provably swallowed
39
- // the noise into a term) and below the anchored grammar (0.9 — which anyway
40
- // gates this strategy off whenever it fires). A distinct class, so disagreement
41
- // with keyword-spot is a ranked winner + "if you mean X then …" alternate,
42
- // never a forced same-class ambiguity over a garbage subject.
5
+ // Fires only when the anchored grammar misses the text as-given. Strips only
6
+ // curated noise or wink-flagged stop words, never a word in KEEP (grammar
7
+ // vocabulary, question scaffolding, context pronouns). Registered as its own
8
+ // class ("noise-stripped") at confidence 0.75 — above keyword-spot, below the
9
+ // anchored grammar.
43
10
 
44
11
  import {
45
12
  VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND, META_MEANING_VERBS,
@@ -73,34 +40,11 @@ const KEEP = new Set([
73
40
  * carried here too so the strategy stands alone) + the cascade's noise list. */
74
41
  const CURATED_NOISE = new Set([...wordsOf(FILLER_WORDS), ...wordsOf(CASCADE_NOISE)]);
75
42
 
76
- /** Words this pass keeps but flags as UNCERTAIN (PLAN_CONVERSATION.md Finding 2
77
- * the "store"/"keep" gap): wink's `isStopWord` is a generic English
78
- * dictionary, not purpose-built for this codebase it happens to flag
79
- * "keep"/"put"/"get" but not their close synonyms "store"/"hold"/"place"/
80
- * "save" sitting in the exact same no-relation-verb slot ("where would i
81
- * keep/store a router"), so a KEPT word surviving the pass above is not
82
- * necessarily real content. A curated synonym list was tried and rejected
83
- * (see the file doc / PLAN_CONVERSATION.md): "store"/"hold"/"save" are
84
- * exactly the words most likely to ALSO be a real identifier ("where does
85
- * the store live" must not lose its subject). The general, non-curated
86
- * signal that discriminates the two: wink's POS tagger, reading the WHOLE
87
- * ORIGINAL sentence for real grammatical context (an isolated 2-word
88
- * fragment like "store router" tags BOTH words NOUN — confirmed live; the
89
- * same "store" in "where would i store a router" tags VERB, and in "where
90
- * does the store live" tags NOUN — also confirmed live, so the isolated
91
- * object phrase alone can never carry this signal; it must be read here,
92
- * off the whole sentence, before the phrase is extracted).
93
- *
94
- * A KEPT word wink tags VERB here is returned as `maybeNoise`, NOT stripped
95
- * outright — this function has no graph to check a resolution against
96
- * (interpret/pipeline.mjs's own documented boundary: "no graph access
97
- * here"). The caller below turns this into a second candidate reading;
98
- * ask.mjs's traverse() (where the graph lives) tries both and prunes the
99
- * one that misses/ties in favor of the one that resolves cleanly —
100
- * mirroring resolveObject's own grain-word retry (try a variant, keep it
101
- * only on an unambiguous hit) and grammar/ace.mjs's parseAceAmbiguous
102
- * ("keep only complete, valid parses"). Never guessed here; always pruned
103
- * where the evidence (the graph) actually is. */
43
+ /** Words this pass keeps but flags as UNCERTAIN: wink's generic isStopWord
44
+ * misses light-verb synonyms ("store"/"hold" for "keep"/"put"), which are
45
+ * also plausible real identifiers, so a whole-sentence POS tag (VERB here)
46
+ * marks them `maybeNoise` rather than stripping them outright — the caller
47
+ * tries both readings and ask.mjs's traverse() prunes using the graph. */
104
48
  function maybeVerbNoiseWords(words, kept, nlp) {
105
49
  if (!nlp || typeof nlp.posTags !== "function" || !kept.length) return [];
106
50
  const keptSet = new Set(kept.map((w) => w.toLowerCase()));
@@ -148,28 +92,13 @@ export const noiseStripStrategy = {
148
92
  if (parseAnchored(text)) return null; // the grammar owns the text as-given
149
93
  const { text: stripped, dropped, maybeNoise } = stripNoise(text, ctx.nlp || null);
150
94
  if (!dropped.length || !stripped) return null;
151
- // tier 1: the anchored templates over the stripped text — the strictest
152
- // re-parse, tried first so a template shape is never displaced by a looser
153
- // decomposition of the same words. tier 2 (cycle-2, CHATBENCH_001 L1): the
154
- // keyword-spot decomposition over the SAME stripped text — the parser the
155
- // clean phrasing of non-template questions ("what calls fnAlpha") actually
156
- // uses, so their noise-wrapped variants recover the identical honest answer
157
- // (see the file doc for the discipline/cost argument).
95
+ // Re-parse: the anchored templates first, then keyword-spot (the parser
96
+ // that clean non-template phrasings like "what calls fnAlpha" already use).
158
97
  const parsed = parseAnchored(stripped) || parseKeywordSpot(stripped, ctx.nlp || null);
159
98
  if (!parsed) return null;
160
- // Finding 2 extension (PLAN_CONVERSATION.md): a bare "where"/"mentions"
161
- // question is the ONE shape with no explicit relation verb gating its
162
- // object (every other decomposition in keywords.mjs requires a real
163
- // VERB_TO_KIND match before it ever runs), so it's the only place an
164
- // unlisted light verb ("store", "hold", …) can leak into the object
165
- // phrase untouched. Scoped to exactly that shape — deliberately not a
166
- // blanket change to stripNoise's shared criteria, per the file's own
167
- // construction-scoping caveat. `altObject` rides along on the SAME
168
- // single candidate (not a second strategy candidate — that would force
169
- // mergeStrategyResults' pre-resolution ambiguousParse surface on every
170
- // hit, before anyone has checked whether it's even real ambiguity);
171
- // ask.mjs's traverse() is the one place both the alternate reading and
172
- // the graph are available together to actually prune it.
99
+ // where/mentions has no relation verb gating its object, so it's the one shape
100
+ // an unlisted light verb can leak into untouched; `altObject` rides on the same
101
+ // candidate so ask.mjs's traverse() can prune using the graph.
173
102
  if (maybeNoise.length && (parsed.shape === "where" || parsed.shape === "mentions") && parsed.object) {
174
103
  const altObject = parsed.object.split(/\s+/).filter((w) => !maybeNoise.includes(w.toLowerCase())).join(" ").trim();
175
104
  if (altObject && altObject !== parsed.object) parsed.altObject = altObject;
@@ -1,40 +1,13 @@
1
- // memory/bias.mjs — bias-weighted fact ranking (extension-pack batch, Part 6).
2
- //
3
- // A small, PURE module, deliberately SEPARATE from trust.mjs's existing closed
4
- // 3-input computeTrust contract (many tests pin computeTrust's exact
5
- // signature; this module never touches it). Trust answers "how much do I
6
- // believe this fact"; bias answers a DIFFERENT question an operator asks
7
- // explicitly — "when two facts disagree, which BUNDLE do I prefer to hear
8
- // from first" (the worked example: is a class part of code, or part of a
9
- // school — a `[bias]` table lets an operator say "for MY repo, weight the
10
- // code-vocabulary bundle over the general-English one").
11
- //
12
- // biasForSourceId(sourceId, biasByBundle) "src:corpus:<name>" -> weight (default 1)
13
- // biasForRow(row, biasByBundle) max bias across row.sourceIds
14
- // rankByBiasThenTrust(rows, biasByBundle) stable: bias desc, trust desc, original order
15
- //
16
- // biasByBundle is the flat `{ bundleName: number }` table src/extensions.mjs's
17
- // resolveExtensions() reads out of tmct.toml's `[bias]` table — resolved ONCE
18
- // per chat session and threaded through, never re-read per turn.
19
- //
20
- // CRITICAL CONTRACT (the operator's own "disclosed, never dropped"
21
- // requirement): bias only REORDERS a hit list. It must NEVER drop, hide, or
22
- // silently prefer a lower-biased fact over a higher-trust one within the same
23
- // bias tier — every hit rankByBiasThenTrust is given still comes back out,
24
- // same length, same members, just reordered.
1
+ // memory/bias.mjs — bias-weighted fact ranking, separate from trust.mjs's
2
+ // computeTrust. `biasByBundle` is the `[bias]` table from tmct.toml. CRITICAL:
3
+ // bias only REORDERS a hit list it must never drop or hide one.
25
4
 
26
- /** A Fact's statedBy Source id is shaped "src:corpus:<bundleName>" for every
27
- * corpus-kind Source (memory/core.mjs's own sourceIdFor see its `corpus`
28
- * case). Any other shape (operator/teach/provider/web/entailed Source ids,
29
- * or a malformed/absent id) is NOT a corpus bundle and always ranks at the
30
- * neutral bias of 1 — bias is a corpus-bundle-only concept, never a proxy
31
- * for a different trust dimension. */
5
+ /** Matches a corpus-kind Source id ("src:corpus:<bundleName>"); anything else
6
+ * is not a corpus bundle and ranks at the neutral bias of 1. */
32
7
  const CORPUS_SOURCE_RE = /^src:corpus:(.+)$/;
33
8
 
34
- /** The bias weight of one Source id, resolved against `biasByBundle`. Default
35
- * 1 (neutral) for a non-corpus source id, an unconfigured bundle name, or a
36
- * non-numeric configured value (never throws — a malformed `[bias]` entry is
37
- * caught earlier, at resolveExtensions() load time). */
9
+ /** The bias weight of one Source id, resolved against `biasByBundle` (default
10
+ * 1 when unconfigured or non-numeric; never throws). */
38
11
  export function biasForSourceId(sourceId, biasByBundle = {}) {
39
12
  const m = CORPUS_SOURCE_RE.exec(String(sourceId || ""));
40
13
  if (!m) return 1;
@@ -42,32 +15,16 @@ export function biasForSourceId(sourceId, biasByBundle = {}) {
42
15
  return typeof v === "number" && Number.isFinite(v) ? v : 1;
43
16
  }
44
17
 
45
- /** The bias weight of one fact ROW (readFactRows' shape: `{..., sourceIds}`)
46
- * the MAX bias across its (possibly several, corroborating) Sources, so a
47
- * fact corroborated by both a neutral and a high-bias bundle ranks at the
48
- * higher of the two, never averaged down. A row with no sourceIds (or an
49
- * empty array) ranks at the neutral 1. */
18
+ /** A fact row's bias: the MAX across its (possibly several) Sources, never
19
+ * averaged down. No sourceIds -> neutral 1. */
50
20
  export function biasForRow(row, biasByBundle = {}) {
51
21
  const ids = Array.isArray(row?.sourceIds) ? row.sourceIds : [];
52
22
  if (!ids.length) return 1;
53
23
  return Math.max(...ids.map((id) => biasForSourceId(id, biasByBundle)));
54
24
  }
55
25
 
56
- /**
57
- * Rank fact rows by bias (desc), then trust (desc), then original relative
58
- * order (STABLE — Array.prototype.sort is stable in Node, reinforced here
59
- * with an explicit index tiebreak so the guarantee never depends on engine
60
- * internals). Every row that goes in comes back out — same length, same
61
- * members — this ONLY reorders, it never filters or drops a hit, honouring
62
- * the "disclosed, never dropped" contract every caller in chat.mjs relies on.
63
- *
64
- * With an empty/absent `biasByBundle` (the default, unconfigured case) every
65
- * row's bias is 1 — a true no-op tier, so the sort degrades to trust-desc,
66
- * ties broken by original order: BYTE-IDENTICAL to today's behaviour for any
67
- * caller that previously sorted by trust alone (or didn't sort at all and
68
- * every row happened to share the same trust, the common single-session
69
- * operator-taught case).
70
- */
26
+ /** Rank fact rows by bias (desc), then trust (desc), then original order
27
+ * (stable) reorders only, never drops a row. */
71
28
  export function rankByBiasThenTrust(rows, biasByBundle = {}) {
72
29
  const list = Array.isArray(rows) ? rows : [];
73
30
  return list