@polycode-projects/the-mechanical-code-talker 1.10.8 → 1.10.11
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/package.json +1 -1
- package/src/chat.mjs +49 -8
- package/src/grammar/ace.mjs +30 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
package/src/chat.mjs
CHANGED
|
@@ -1532,7 +1532,7 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
1532
1532
|
// assert/memory path; when it can't be stored, say what CAN be remembered
|
|
1533
1533
|
// instead of the grammar wall or a silent data loss.
|
|
1534
1534
|
const TEACH_RE = /^(?:please\s+)?(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+would)\s+like\s+you\s+to\s+)?(?:remember|note|keep in mind|jot down|for the record|fyi|learn)\b(?:\s+(?:this|that|also))?[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
1535
|
-
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+ (?:is|are) (?:a |an )?[\w-]+$/i;
|
|
1535
|
+
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:a |an )?[\w-]+$/i;
|
|
1536
1536
|
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
1537
1537
|
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
1538
1538
|
const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
@@ -5663,7 +5663,8 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
5663
5663
|
// all (no fact row on either side, no code entity by id OR class noun) —
|
|
5664
5664
|
// a subject known via OTHER predicates ("ahab is male") or the code graph
|
|
5665
5665
|
// keeps the old decline, so nothing downstream is ever shadowed.
|
|
5666
|
-
if (!ent && !noun && !
|
|
5666
|
+
if (!ent && !noun && !isPronoun(subjectWord)
|
|
5667
|
+
&& !rows.some((f) => subjCandidates.has(f.subject) || subjCandidates.has(f.object))) {
|
|
5667
5668
|
return {
|
|
5668
5669
|
text: `I can't confirm that — I don't know "${subjectWord}" at all yet. If it's true, teach me: "${subjectWord} is a kind of ${kindWord}".`,
|
|
5669
5670
|
replace: true,
|
|
@@ -6950,7 +6951,7 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
6950
6951
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
6951
6952
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
6952
6953
|
* normal answer, never a crash. */
|
|
6953
|
-
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null }) {
|
|
6954
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null }) {
|
|
6954
6955
|
const ts = new Date().toISOString();
|
|
6955
6956
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
6956
6957
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
@@ -7238,7 +7239,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7238
7239
|
}
|
|
7239
7240
|
}
|
|
7240
7241
|
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus;
|
|
7241
|
-
|
|
7242
|
+
// A turn whose pronoun was bound to a vocabulary antecedent is PROVABLY a
|
|
7243
|
+
// fact question ("can it bark" → "can dog bark") — never conversational,
|
|
7244
|
+
// however short. Without this, the substituted 3-worder still trips
|
|
7245
|
+
// isConversational's word-count catch-all into the orientation blurb, and
|
|
7246
|
+
// that blurb (a dispatched turn) then becomes `last`, wiping the very
|
|
7247
|
+
// antecedent the next pronoun turn needs.
|
|
7248
|
+
const isConversationalCandidate = conversationalCandidateBaseGate && !vocabAntecedent && isConversational(query);
|
|
7242
7249
|
// "what is X" with NO article ("what is john") is BOTH conversational-shaped
|
|
7243
7250
|
// (isConversational() would claim it) AND a legitimate bare meta/fact-lookup
|
|
7244
7251
|
// form (BARE_WHATIS_RE). Diverts ONLY when a REAL fact actually resolves for
|
|
@@ -8113,6 +8120,21 @@ function rewriteUsesAsBaseFrame(text) {
|
|
|
8113
8120
|
return null;
|
|
8114
8121
|
}
|
|
8115
8122
|
|
|
8123
|
+
/** The subject of the LAST turn's first fact line, for vocabulary pronoun
|
|
8124
|
+
* binding ("what is a dog" → "can it bark"). Fact answers render rigidly —
|
|
8125
|
+
* "<subject> <phrase> <object> (source: …)", optionally behind a "yes — "/
|
|
8126
|
+
* "no — "/"you told me: " prefix — so a 1–2 word leading subject followed
|
|
8127
|
+
* by a phrase-table verb is extractable without any NLP. Anything else
|
|
8128
|
+
* (code answers, walls, conversational text) returns null and no
|
|
8129
|
+
* substitution happens. */
|
|
8130
|
+
function vocabAntecedentFrom(last) {
|
|
8131
|
+
const first = String(last?.answer || "").split("\n")[0]
|
|
8132
|
+
.replace(/^(?:yes|no) — /i, "")
|
|
8133
|
+
.replace(/^you told me: /i, "");
|
|
8134
|
+
const m = first.match(/^([a-z][\w'-]*(?:\s+[a-z][\w'-]*)?)\s+(?:is|are|has|can|causes|wants|requires|involves|means|begins|ends)\b/i);
|
|
8135
|
+
return m ? m[1] : null;
|
|
8136
|
+
}
|
|
8137
|
+
|
|
8116
8138
|
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null } = {}) {
|
|
8117
8139
|
const line = String(input ?? "").trim();
|
|
8118
8140
|
// ONE fresh, empty cache for this turn only — every factRows() reader
|
|
@@ -8130,13 +8152,32 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8130
8152
|
// before ANY dispatch lane sees the text. Null (no-op) for every turn that
|
|
8131
8153
|
// doesn't match one of the four discontiguous shapes.
|
|
8132
8154
|
const baseFrameRewrite = rewriteUsesAsBaseFrame(preRewriteLine);
|
|
8133
|
-
const
|
|
8155
|
+
const frameLine = baseFrameRewrite || preRewriteLine;
|
|
8156
|
+
// VOCABULARY pronoun antecedent — "what is a dog" then "can it bark". The
|
|
8157
|
+
// code-graph focus mechanism only ever binds {id,label} GRAPH entities, so
|
|
8158
|
+
// in a vocabulary conversation "it" resolved to nothing and the question
|
|
8159
|
+
// fell to the conversational gate or a garbage-subject fact lookup.
|
|
8160
|
+
// Substituted here, once, before any dispatch lane sees the text — and
|
|
8161
|
+
// ONLY when no code focus is standing (a graph session's own pronoun
|
|
8162
|
+
// resolution is untouched), the turn looks like a fact question, and the
|
|
8163
|
+
// LAST answer's own first fact line names a subject to bind to.
|
|
8164
|
+
// Anchored to SUBJECT position only: the pronoun must directly follow the
|
|
8165
|
+
// opening auxiliary ("can it bark") or "what is/are" WITH a continuation
|
|
8166
|
+
// ("what is it used for") — so idioms carrying a trailing dummy pronoun
|
|
8167
|
+
// ("what time is it") and the bare "what is it" are never rewritten.
|
|
8168
|
+
const pronounLead = frameLine.match(/^((?:is|are|can|could|does|do)\s+|what\s+(?:is|are)\s+)(?:it|they)\b(\s+\S.*)?$/i);
|
|
8169
|
+
const vocabAntecedent = (!focus?.id && memoryDir && pronounLead
|
|
8170
|
+
&& !(/^what/i.test(pronounLead[1]) && !pronounLead[2]))
|
|
8171
|
+
? vocabAntecedentFrom(last) : null;
|
|
8172
|
+
const workingLine = vocabAntecedent
|
|
8173
|
+
? `${pronounLead[1]}${vocabAntecedent}${pronounLead[2] || ""}`
|
|
8174
|
+
: frameLine;
|
|
8134
8175
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
8135
8176
|
const trace = narrate ? [] : null;
|
|
8136
8177
|
// vocabHint: createSession computes this ONCE per session; a direct
|
|
8137
8178
|
// runTurn() caller that doesn't pass one gets it computed here instead.
|
|
8138
8179
|
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
|
|
8139
|
-
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache };
|
|
8180
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent };
|
|
8140
8181
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
8141
8182
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
8142
8183
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -8149,7 +8190,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8149
8190
|
// indirect-request wrapper stripped and/or the discontiguous-frame
|
|
8150
8191
|
// rewrite applied) — restore the ORIGINAL raw `line` into record.query
|
|
8151
8192
|
// and the logged transcript echo here, once, centrally.
|
|
8152
|
-
if (indirectMatch || baseFrameRewrite) {
|
|
8193
|
+
if (indirectMatch || baseFrameRewrite || vocabAntecedent) {
|
|
8153
8194
|
if (finished.record) finished.record.query = line;
|
|
8154
8195
|
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
8155
8196
|
}
|
|
@@ -8174,7 +8215,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8174
8215
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
8175
8216
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
8176
8217
|
// narrate block is applied directly here instead.
|
|
8177
|
-
const convo = conversationalTurn(workingLine, ctx);
|
|
8218
|
+
const convo = vocabAntecedent ? null : conversationalTurn(workingLine, ctx);
|
|
8178
8219
|
if (convo) return withNarration(convo, trace, "casual/social — no graph intent");
|
|
8179
8220
|
|
|
8180
8221
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
package/src/grammar/ace.mjs
CHANGED
|
@@ -95,7 +95,7 @@ const stripDet = (tokens) =>
|
|
|
95
95
|
* with "a"/"an" — the one signal that a singular-plural-fold collision
|
|
96
96
|
* (die/dice, person/people, tooth/teeth) can be resolved by, rather than
|
|
97
97
|
* silently committing to whichever the lexicon happens to fold to first. */
|
|
98
|
-
function resolveNP(lexicon, tokensIn) {
|
|
98
|
+
function resolveNP(lexicon, tokensIn, { allowCompound = false } = {}) {
|
|
99
99
|
const ns = lexicon.ns;
|
|
100
100
|
const singularOnly = tokensIn.length > 1 && SINGULAR_ONLY_DET.has(tokensIn[0].toLowerCase());
|
|
101
101
|
const tokens = stripDet(tokensIn);
|
|
@@ -131,6 +131,26 @@ function resolveNP(lexicon, tokensIn) {
|
|
|
131
131
|
}
|
|
132
132
|
return { term, individual: false, noun, extras, unknown: [] };
|
|
133
133
|
}
|
|
134
|
+
// Two plain NOUNS in a row are ONE compound noun ("guinea pig", "sports
|
|
135
|
+
// car"), space-joined to match the corpus's own multi-word concepts
|
|
136
|
+
// ("schema person"), so the taught fact and the query side unify.
|
|
137
|
+
// STRICTLY OPT-IN per call site (allowCompound): only the patterns where
|
|
138
|
+
// a compound subject is safe request it — capability, quantified
|
|
139
|
+
// membership, disjointness, and the articled-complement copula. The
|
|
140
|
+
// generic relation walk and the bare-adjective copula never do, so a
|
|
141
|
+
// question lead ("does dog have…") or a property sentence ("checkout
|
|
142
|
+
// flow is deprecated") can never silently become an ACE teach. A
|
|
143
|
+
// DECLARED proper name in either slot ("GitLab pipeline") keeps the
|
|
144
|
+
// structural miss below — a name in the wrong slot, not a compound.
|
|
145
|
+
if (allowCompound
|
|
146
|
+
&& !lookupProperName(lexicon, tokens[0]) && !lookupProperName(lexicon, tokens[1])
|
|
147
|
+
&& /^[a-z][a-z'-]*$/i.test(tokens[0]) && /^[a-z][a-z'-]*$/i.test(tokens[1])) {
|
|
148
|
+
const n0 = lookupNoun(lexicon, tokens[0], { singularOnly: false });
|
|
149
|
+
const n1 = lookupNoun(lexicon, tokens[1], { singularOnly });
|
|
150
|
+
if (n0 && n1) {
|
|
151
|
+
return { term: `${ns}${tokens[0].toLowerCase()} ${tokens[1].toLowerCase()}`, individual: false, noun: n1, extras: [], unknown: [] };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
134
154
|
// only genuinely undeclared words are residue — a declared word in the
|
|
135
155
|
// wrong slot ("GitLab pipeline") is a structural miss, not an unknown
|
|
136
156
|
const unknown = tokens.filter((t) => !classify(t, lexicon));
|
|
@@ -326,13 +346,11 @@ function parseEvery(lexicon, toks, lower) {
|
|
|
326
346
|
}
|
|
327
347
|
const isIdx = lower.indexOf("is");
|
|
328
348
|
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
329
|
-
const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
|
|
330
349
|
const rest = toks.slice(isIdx + 1);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
const np2 = resolveNP(lexicon, rest);
|
|
350
|
+
const everyAdjOnly = rest.length === 1 ? lookupAdjective(lexicon, rest[0]) : null;
|
|
351
|
+
const np1 = resolveNP(lexicon, toks.slice(1, isIdx), { allowCompound: !everyAdjOnly });
|
|
352
|
+
if (everyAdjOnly) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, everyAdjOnly);
|
|
353
|
+
const np2 = resolveNP(lexicon, rest, { allowCompound: true });
|
|
336
354
|
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_SUB_CLASS_OF, [np1, np2]);
|
|
337
355
|
if (np1.individual || np2.individual) return null; // "every X is chat.mjs" — not the fragment
|
|
338
356
|
return hit(PATTERN_SUB_CLASS_OF, [np1, np2], [
|
|
@@ -344,8 +362,8 @@ function parseEvery(lexicon, toks, lower) {
|
|
|
344
362
|
function parseDisjoint(lexicon, toks, lower) {
|
|
345
363
|
const isIdx = lower.indexOf("is");
|
|
346
364
|
if (isIdx <= 1 || isIdx === toks.length - 1) return null;
|
|
347
|
-
const np1 = resolveNP(lexicon, toks.slice(1, isIdx));
|
|
348
|
-
const np2 = resolveNP(lexicon, toks.slice(isIdx + 1));
|
|
365
|
+
const np1 = resolveNP(lexicon, toks.slice(1, isIdx), { allowCompound: true });
|
|
366
|
+
const np2 = resolveNP(lexicon, toks.slice(isIdx + 1), { allowCompound: true });
|
|
349
367
|
if (np1.term == null || np2.term == null) return missOrNull(PATTERN_DISJOINT_WITH, [np1, np2]);
|
|
350
368
|
if (np1.individual || np2.individual) return null;
|
|
351
369
|
return hit(PATTERN_DISJOINT_WITH, [np1, np2], [
|
|
@@ -393,14 +411,14 @@ function parseOfForm(lexicon, toks, lower) {
|
|
|
393
411
|
|
|
394
412
|
/** Patterns 2 (class assertion), 1's bare-copula variant, and 8's copula arm. */
|
|
395
413
|
function parseCopula(lexicon, toks, lower, isIdx) {
|
|
396
|
-
const np1 = resolveNP(lexicon, toks.slice(0, isIdx));
|
|
397
414
|
const rest = toks.slice(isIdx + 1);
|
|
398
415
|
if (!rest.length) return null;
|
|
416
|
+
const np1 = resolveNP(lexicon, toks.slice(0, isIdx), { allowCompound: rest.length > 1 });
|
|
399
417
|
if (rest.length === 1) {
|
|
400
418
|
const adj = lookupAdjective(lexicon, rest[0]);
|
|
401
419
|
if (adj) return adjectiveCopula(lexicon, PATTERN_ADJECTIVE, np1, adj);
|
|
402
420
|
}
|
|
403
|
-
const np2 = resolveNP(lexicon, rest);
|
|
421
|
+
const np2 = resolveNP(lexicon, rest, { allowCompound: true });
|
|
404
422
|
if (np1.term == null || np2.term == null) {
|
|
405
423
|
return missOrNull(np1.individual ? PATTERN_TYPE_ASSERTION : PATTERN_SUB_CLASS_OF, [np1, np2]);
|
|
406
424
|
}
|
|
@@ -448,7 +466,7 @@ export function parseAce(sentence, lexicon = loadLexicon()) {
|
|
|
448
466
|
* has no negative-capability predicate, and a silently dropped negation
|
|
449
467
|
* would invert the taught meaning. */
|
|
450
468
|
function parseCapability(lexicon, toks, canIdx) {
|
|
451
|
-
const np1 = resolveNP(lexicon, toks.slice(0, canIdx));
|
|
469
|
+
const np1 = resolveNP(lexicon, toks.slice(0, canIdx), { allowCompound: true });
|
|
452
470
|
if (np1.term == null) return null;
|
|
453
471
|
// The capability's object is a VERB ("swim"), not a lexicon noun, so
|
|
454
472
|
// resolveNP is the wrong resolver for it: accept exactly one bare word,
|