@polycode-projects/the-mechanical-code-talker 1.10.7 → 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.
- package/package.json +1 -1
- package/src/chat.mjs +120 -7
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.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
|
@@ -4166,6 +4166,42 @@ const REVERSE_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
|
4166
4166
|
re: new RegExp(`^what\\s+${escapeRegex(phrase)}\\s+(.+?)[?.!\\s]*$`, "i"),
|
|
4167
4167
|
}))
|
|
4168
4168
|
.sort((a, b) => b.re.source.length - a.re.source.length); // longest phrase first
|
|
4169
|
+
|
|
4170
|
+
// The FORWARD yes/no mirror of REVERSE_PREDICATE_MARKERS: one derived
|
|
4171
|
+
// "is/are X <phrase> Y" (copula phrases), "can X be Y" (receivesAction), or
|
|
4172
|
+
// "does/do X <base-verb> Y" (verb phrases, naive de-3sg fold) reader per
|
|
4173
|
+
// FACT_PREDICATE_PHRASES entry — so every relation the table can RENDER can
|
|
4174
|
+
// also be ASKED as a forward yes/no, instead of each one needing its own
|
|
4175
|
+
// hand-written lane. Excluded: the isa family and hasProperty (ISA_ASK_RE /
|
|
4176
|
+
// IS_ADJECTIVE_YESNO_RE territory), hasA and capableOf (their dedicated
|
|
4177
|
+
// readers above carry teach hints these derived ones deliberately don't —
|
|
4178
|
+
// no derived hint is emitted because no teach phrasing for these relations
|
|
4179
|
+
// is verified to round-trip).
|
|
4180
|
+
const FORWARD_YESNO_EXCLUDE = new Set([
|
|
4181
|
+
"rdfs:subClassOf", "rdf:type", "owl:disjointWith", "mgx:hasProperty",
|
|
4182
|
+
"mgx:hasA", "mgx:capableOf",
|
|
4183
|
+
// ownership's dedicated reader (OWNS_YESNO_RE) answers a confident
|
|
4184
|
+
// closed-world "no" — a stronger contract than the derived "can't
|
|
4185
|
+
// confirm", so the derived reader must never intercept it.
|
|
4186
|
+
"mgx:ownedBy",
|
|
4187
|
+
]);
|
|
4188
|
+
const FORWARD_YESNO_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
4189
|
+
.filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate))
|
|
4190
|
+
.map(([predicate, phrase]) => {
|
|
4191
|
+
let re;
|
|
4192
|
+
if (phrase === "can be") {
|
|
4193
|
+
re = new RegExp("^can\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+be\\s+(.+?)[?.!\\s]*$", "i");
|
|
4194
|
+
} else if (phrase.startsWith("is ")) {
|
|
4195
|
+
const rest = escapeRegex(phrase.slice(3));
|
|
4196
|
+
re = new RegExp(`^(?:is|are)\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+${rest}\\s+(?:an?\\s+|the\\s+)?(.+?)[?.!\\s]*$`, "i");
|
|
4197
|
+
} else {
|
|
4198
|
+
const [head, ...tail] = phrase.split(" ");
|
|
4199
|
+
const base = [head.replace(/s$/, ""), ...tail].map(escapeRegex).join("\\s+");
|
|
4200
|
+
re = new RegExp(`^(?:does|do)\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+${base}\\s+(?:an?\\s+|the\\s+)?(.+?)[?.!\\s]*$`, "i");
|
|
4201
|
+
}
|
|
4202
|
+
return { predicate, phrase, re };
|
|
4203
|
+
})
|
|
4204
|
+
.sort((a, b) => b.re.source.length - a.re.source.length); // longest phrase first
|
|
4169
4205
|
// On the FIRST turn of a graph-less session, `envelope` stays null for the
|
|
4170
4206
|
// whole turn (dispatchTool's loadGraph() throws its own documented empty-graph
|
|
4171
4207
|
// ToolError, self-correcting from turn 2 on), so this regex is the ONLY path
|
|
@@ -4330,6 +4366,42 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4330
4366
|
}
|
|
4331
4367
|
if (!miss) return null;
|
|
4332
4368
|
|
|
4369
|
+
// (b0) Derived forward yes/no readers — FORWARD_YESNO_MARKERS, one per
|
|
4370
|
+
// renderable relation. Runs BEFORE the isa lane because ISA_ASK_RE's lazy
|
|
4371
|
+
// subject otherwise swallows these shapes whole ("is a wheel part of a
|
|
4372
|
+
// car" reads as subject "wheel part of") and ends the cascade. A real fact
|
|
4373
|
+
// answers yes; a subject known under the SAME relation gets an honest miss
|
|
4374
|
+
// citing those facts; a subject known at all (with no structural parse
|
|
4375
|
+
// standing) gets a bare honest miss; anything else leaves the standing
|
|
4376
|
+
// miss text alone — so a code-shaped query with a real parse is never
|
|
4377
|
+
// hijacked.
|
|
4378
|
+
for (const { predicate, phrase, re } of FORWARD_YESNO_MARKERS) {
|
|
4379
|
+
const m = q.match(re);
|
|
4380
|
+
if (!m) continue;
|
|
4381
|
+
const facts = await memoryFacts(memoryDir);
|
|
4382
|
+
const subj = factTermVariants(normFactTerm, m[1]);
|
|
4383
|
+
const obj = factTermVariants(normFactTerm, m[2]);
|
|
4384
|
+
const hit = facts.find((f) => f.predicate === predicate && subj.has(f.subject) && obj.has(f.object));
|
|
4385
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
4386
|
+
const sameRelation = facts.filter((f) => f.predicate === predicate && subj.has(f.subject));
|
|
4387
|
+
if (sameRelation.length) {
|
|
4388
|
+
const shown = sameRelation.slice(0, 3).map(renderFactLine).join("; ");
|
|
4389
|
+
return {
|
|
4390
|
+
text: `I can't confirm that — nothing I remember says ${m[1]} ${phrase} ${m[2]}. I do know: ${shown}.`,
|
|
4391
|
+
replace: true,
|
|
4392
|
+
miss: true,
|
|
4393
|
+
};
|
|
4394
|
+
}
|
|
4395
|
+
if (!envelope?.parsed && facts.some((f) => subj.has(f.subject))) {
|
|
4396
|
+
return {
|
|
4397
|
+
text: `I can't confirm that — nothing I remember says ${m[1]} ${phrase} ${m[2]}.`,
|
|
4398
|
+
replace: true,
|
|
4399
|
+
miss: true,
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
break; // shape matched, nothing honest to add — the standing miss stands
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4333
4405
|
// (b) "is a module a component" — yes iff a remembered isa-family fact says so.
|
|
4334
4406
|
// Also accepts "why is X a Y" / "explain how you know X is Y" — see matchWhyIsa.
|
|
4335
4407
|
const isa = q.match(ISA_ASK_RE) || matchWhyIsa(q);
|
|
@@ -5591,7 +5663,8 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
5591
5663
|
// all (no fact row on either side, no code entity by id OR class noun) —
|
|
5592
5664
|
// a subject known via OTHER predicates ("ahab is male") or the code graph
|
|
5593
5665
|
// keeps the old decline, so nothing downstream is ever shadowed.
|
|
5594
|
-
if (!ent && !noun && !
|
|
5666
|
+
if (!ent && !noun && !isPronoun(subjectWord)
|
|
5667
|
+
&& !rows.some((f) => subjCandidates.has(f.subject) || subjCandidates.has(f.object))) {
|
|
5595
5668
|
return {
|
|
5596
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}".`,
|
|
5597
5670
|
replace: true,
|
|
@@ -6878,7 +6951,7 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
6878
6951
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
6879
6952
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
6880
6953
|
* normal answer, never a crash. */
|
|
6881
|
-
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 }) {
|
|
6882
6955
|
const ts = new Date().toISOString();
|
|
6883
6956
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
6884
6957
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
@@ -7166,7 +7239,13 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7166
7239
|
}
|
|
7167
7240
|
}
|
|
7168
7241
|
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus;
|
|
7169
|
-
|
|
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);
|
|
7170
7249
|
// "what is X" with NO article ("what is john") is BOTH conversational-shaped
|
|
7171
7250
|
// (isConversational() would claim it) AND a legitimate bare meta/fact-lookup
|
|
7172
7251
|
// form (BARE_WHATIS_RE). Diverts ONLY when a REAL fact actually resolves for
|
|
@@ -8041,6 +8120,21 @@ function rewriteUsesAsBaseFrame(text) {
|
|
|
8041
8120
|
return null;
|
|
8042
8121
|
}
|
|
8043
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
|
+
|
|
8044
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 } = {}) {
|
|
8045
8139
|
const line = String(input ?? "").trim();
|
|
8046
8140
|
// ONE fresh, empty cache for this turn only — every factRows() reader
|
|
@@ -8058,13 +8152,32 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8058
8152
|
// before ANY dispatch lane sees the text. Null (no-op) for every turn that
|
|
8059
8153
|
// doesn't match one of the four discontiguous shapes.
|
|
8060
8154
|
const baseFrameRewrite = rewriteUsesAsBaseFrame(preRewriteLine);
|
|
8061
|
-
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;
|
|
8062
8175
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
8063
8176
|
const trace = narrate ? [] : null;
|
|
8064
8177
|
// vocabHint: createSession computes this ONCE per session; a direct
|
|
8065
8178
|
// runTurn() caller that doesn't pass one gets it computed here instead.
|
|
8066
8179
|
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
|
|
8067
|
-
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 };
|
|
8068
8181
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
8069
8182
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
8070
8183
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -8077,7 +8190,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8077
8190
|
// indirect-request wrapper stripped and/or the discontiguous-frame
|
|
8078
8191
|
// rewrite applied) — restore the ORIGINAL raw `line` into record.query
|
|
8079
8192
|
// and the logged transcript echo here, once, centrally.
|
|
8080
|
-
if (indirectMatch || baseFrameRewrite) {
|
|
8193
|
+
if (indirectMatch || baseFrameRewrite || vocabAntecedent) {
|
|
8081
8194
|
if (finished.record) finished.record.query = line;
|
|
8082
8195
|
if (Array.isArray(finished.logLines) && finished.logLines.length > 1) finished.logLines[1] = `> ${line}`;
|
|
8083
8196
|
}
|
|
@@ -8102,7 +8215,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8102
8215
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
8103
8216
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
8104
8217
|
// narrate block is applied directly here instead.
|
|
8105
|
-
const convo = conversationalTurn(workingLine, ctx);
|
|
8218
|
+
const convo = vocabAntecedent ? null : conversationalTurn(workingLine, ctx);
|
|
8106
8219
|
if (convo) return withNarration(convo, trace, "casual/social — no graph intent");
|
|
8107
8220
|
|
|
8108
8221
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|