@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.
- package/README.md +441 -202
- package/bin/tmct.mjs +126 -1
- 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 -2872
- 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,7 +1,7 @@
|
|
|
1
|
-
// interpret/strategies/keywords.mjs — strategy 2: keyword-spotting/decomposition
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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:
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
85
|
-
//
|
|
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 (
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
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
|
-
//
|
|
143
|
-
//
|
|
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
|
|
154
|
-
//
|
|
155
|
-
//
|
|
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
|
|
176
|
-
//
|
|
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
|
|
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
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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
|
-
//
|
|
230
|
-
// agent-marking "by"
|
|
231
|
-
//
|
|
232
|
-
//
|
|
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
|
-
//
|
|
267
|
-
//
|
|
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"
|
|
281
|
-
//
|
|
282
|
-
//
|
|
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 (
|
|
301
|
-
//
|
|
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 —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
-
//
|
|
152
|
-
//
|
|
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
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
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;
|
package/src/memory/bias.mjs
CHANGED
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
// memory/bias.mjs — bias-weighted fact ranking
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
/**
|
|
27
|
-
* 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
|
|
35
|
-
* 1
|
|
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
|
-
/**
|
|
46
|
-
*
|
|
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
|
-
*
|
|
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
|