@polycode-projects/the-mechanical-code-talker 1.10.8 → 1.10.10

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/chat.mjs +48 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.10.8",
3
+ "version": "1.10.10",
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
@@ -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 && !rows.some((f) => subjCandidates.has(f.subject) || subjCandidates.has(f.object))) {
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
- const isConversationalCandidate = conversationalCandidateBaseGate && isConversational(query);
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 workingLine = baseFrameRewrite || preRewriteLine;
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