@polycode-projects/the-mechanical-code-talker 1.10.12 → 1.10.14
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 +106 -11
- package/src/finish.mjs +8 -4
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.14",
|
|
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
|
@@ -1533,6 +1533,12 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
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
1535
|
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:a |an )?[\w-]+$/i;
|
|
1536
|
+
/** "X is <comparative> than Y" — the comparative teach/ask surface. The
|
|
1537
|
+
* comparative slot is closed by SHAPE (-er word, better/worse, or a
|
|
1538
|
+
* more/less + adjective pair), never a hand-list of adjectives. */
|
|
1539
|
+
const COMPARATIVE_SRC = "(?:[a-z]+er|better|worse|(?:more|less)\\s+[a-z]+)";
|
|
1540
|
+
const COMPARATIVE_TEACH_RE = new RegExp(`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(?:is|are)\\s+(${COMPARATIVE_SRC})\\s+than\\s+(.+)$`, "i");
|
|
1541
|
+
const COMPARATIVE_ASK_RE = new RegExp(`^(?:is|are)\\s+(.+?)\\s+(${COMPARATIVE_SRC})\\s+than\\s+(.+?)[?.!\\s]*$`, "i");
|
|
1536
1542
|
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
1537
1543
|
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
1538
1544
|
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;
|
|
@@ -2354,10 +2360,10 @@ async function generalVerbTeach(payload) {
|
|
|
2354
2360
|
if (verb === "cannot") return null;
|
|
2355
2361
|
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) return null; // not a bare-name subject
|
|
2356
2362
|
const subject = subjectRaw.trim();
|
|
2357
|
-
const
|
|
2363
|
+
const folded = foldPrepositionIntoPredicate(await generalVerbPredicate(verb), objectRaw);
|
|
2364
|
+
const object = folded.object.replace(/^an?\s+/i, "").trim();
|
|
2358
2365
|
if (!subject || !object) return null; // no well-formed triple — honest decline (point 6)
|
|
2359
|
-
|
|
2360
|
-
return { subject, predicate, object };
|
|
2366
|
+
return { subject, predicate: folded.predicate, object };
|
|
2361
2367
|
}
|
|
2362
2368
|
|
|
2363
2369
|
/** Is `word` a genuine NOUN/PROPN, per wink-nlp's optional POS tagger
|
|
@@ -2391,7 +2397,7 @@ async function subjectIsNounOrPropn(word) {
|
|
|
2391
2397
|
// query is never shadowed. Reuses generalVerbTeach's own exclude guards and
|
|
2392
2398
|
// generalVerbPredicate, plus the SAME adverb-skip, so the two never disagree. ----
|
|
2393
2399
|
const GENERAL_VERB_YESNO_RE = new RegExp(`^(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[?.!\\s]*$`, "i");
|
|
2394
|
-
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)[?.!\\s]*$`, "i");
|
|
2400
|
+
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+(?:\\s+(?:on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside))?)[?.!\\s]*$`, "i");
|
|
2395
2401
|
/** GENERAL_VERB_EXCLUDE_RE was written for generalVerbTeach's fully-conjugated
|
|
2396
2402
|
* declarative verb ("X OWNS Y", "X MAINTAINS Y") — but "does/did X <verb> Y"
|
|
2397
2403
|
* captures the BARE INFINITIVE after do-support ("does X OWN Y", never "does X
|
|
@@ -2403,6 +2409,23 @@ const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+
|
|
|
2403
2409
|
* The query-side guard needs the bare-infinitive counterpart too. */
|
|
2404
2410
|
const GENERAL_VERB_QUERY_EXCLUDE_RE = /^(?:be|own|maintain)$/i;
|
|
2405
2411
|
|
|
2412
|
+
/** Closed prepositions the general-verb teach/query lanes FOLD INTO the
|
|
2413
|
+
* minted predicate: "disk-1 rests on peg-a" stores mgx:rest-on with object
|
|
2414
|
+
* "peg-a", never mgx:rest with the meaning-bearing "on" buried inside the
|
|
2415
|
+
* object where no read-back can match it. */
|
|
2416
|
+
const GENERAL_VERB_PREP_RE = /^(on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside)\s+(.+)$/i;
|
|
2417
|
+
/** Fold a leading preposition from `objectRaw` into a minted mgx:<lemma>
|
|
2418
|
+
* predicate. Curated predicates (mgx:hasA, mgx:capableOf — anything not the
|
|
2419
|
+
* plain lowercase mint shape) are never suffixed. Returns {predicate,
|
|
2420
|
+
* object} either way. */
|
|
2421
|
+
function foldPrepositionIntoPredicate(predicate, objectRaw) {
|
|
2422
|
+
const prepM = String(objectRaw || "").match(GENERAL_VERB_PREP_RE);
|
|
2423
|
+
if (prepM && /^mgx:[a-z]+$/.test(predicate)) {
|
|
2424
|
+
return { predicate: `${predicate}-${prepM[1].toLowerCase()}`, object: prepM[2].trim() };
|
|
2425
|
+
}
|
|
2426
|
+
return { predicate, object: String(objectRaw || "").trim() };
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2406
2429
|
/** Sentence forms to try asserting for a teach payload: the payload as-is, and
|
|
2407
2430
|
* (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
|
|
2408
2431
|
* grammar actually lands. */
|
|
@@ -2862,7 +2885,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2862
2885
|
|
|
2863
2886
|
let payload = null;
|
|
2864
2887
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
2865
|
-
else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
2888
|
+
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)) && !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
2866
2889
|
if (!payload) {
|
|
2867
2890
|
// "remember margo eats ribs", re-escaping here through a combination
|
|
2868
2891
|
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
@@ -2880,6 +2903,19 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2880
2903
|
// Try to store it (a live session provides the write target). assertTurn returns
|
|
2881
2904
|
// the "noted — remembered …" confirmation or null (grammar miss / unknown words).
|
|
2882
2905
|
if (memoryDir) {
|
|
2906
|
+
// COMPARATIVE frame — "disk-1 is smaller than disk-2" → mgx:smaller-than.
|
|
2907
|
+
// Checked ahead of the ACE candidates: the copula plus "than" is not in
|
|
2908
|
+
// the ACE fragment at all, and letting it fall through produced the
|
|
2909
|
+
// both-sides-ungrounded decline (honest but unactionable — no phrasing
|
|
2910
|
+
// it could suggest would have stored a comparison).
|
|
2911
|
+
const comp = String(payload).trim().match(COMPARATIVE_TEACH_RE);
|
|
2912
|
+
if (comp) {
|
|
2913
|
+
const compPredicate = `mgx:${comp[2].toLowerCase().replace(/\s+/g, "-")}-than`;
|
|
2914
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
2915
|
+
subject: comp[1].trim(), predicate: compPredicate, object: comp[3].trim(),
|
|
2916
|
+
});
|
|
2917
|
+
if (stored) return stored;
|
|
2918
|
+
}
|
|
2883
2919
|
for (const cand of assertCandidates(payload)) {
|
|
2884
2920
|
// assertTurn ITSELF records the "every" quantifier (point 3) on a plain
|
|
2885
2921
|
// universal success, so every caller (this loop AND the top-level
|
|
@@ -3729,8 +3765,15 @@ function thirdPersonSingularSurface(lemma) {
|
|
|
3729
3765
|
}
|
|
3730
3766
|
function predicatePhrase(predicate) {
|
|
3731
3767
|
if (FACT_PREDICATE_PHRASES[predicate]) return FACT_PREDICATE_PHRASES[predicate];
|
|
3732
|
-
const
|
|
3733
|
-
|
|
3768
|
+
const p = String(predicate || "");
|
|
3769
|
+
// a comparative renders as its copula surface: mgx:smaller-than ->
|
|
3770
|
+
// "is smaller than" (never a 3sg fold — "smallers" isn't a word)
|
|
3771
|
+
const comp = /^mgx:([a-z]+(?:-[a-z]+)*)-than$/i.exec(p);
|
|
3772
|
+
if (comp) return `is ${comp[1].replace(/-/g, " ")} than`;
|
|
3773
|
+
const m = /^mgx:([a-z]+)(?:-([a-z]+))?$/i.exec(p);
|
|
3774
|
+
if (!m) return predicate;
|
|
3775
|
+
// a folded preposition renders back naturally: mgx:rest-on -> "rests on"
|
|
3776
|
+
return `${thirdPersonSingularSurface(m[1])}${m[2] ? ` ${m[2]}` : ""}`;
|
|
3734
3777
|
}
|
|
3735
3778
|
const factPhrase = (f) => `${f.subject} ${predicatePhrase(f.predicate)} ${f.object}`;
|
|
3736
3779
|
|
|
@@ -4366,6 +4409,30 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4366
4409
|
}
|
|
4367
4410
|
if (!miss) return null;
|
|
4368
4411
|
|
|
4412
|
+
// (b0-comp) "is disk-1 smaller than disk-2" — yes iff the exact taught
|
|
4413
|
+
// comparative fact exists; otherwise an honest, specific miss whose teach
|
|
4414
|
+
// hint is the EXACT phrasing the comparative teach frame accepts. Never an
|
|
4415
|
+
// inverted guess: "disk-1 is smaller than disk-2" proves nothing here
|
|
4416
|
+
// about "is disk-2 smaller than disk-1" (the frame stores no
|
|
4417
|
+
// antisymmetry), so the reverse question stays a can't-confirm.
|
|
4418
|
+
const compAsk = q.match(COMPARATIVE_ASK_RE);
|
|
4419
|
+
if (compAsk) {
|
|
4420
|
+
const compWord = compAsk[2].toLowerCase().replace(/\s+/g, "-");
|
|
4421
|
+
const compPredicate = `mgx:${compWord}-than`;
|
|
4422
|
+
const facts = await memoryFacts(memoryDir);
|
|
4423
|
+
const subj = factTermVariants(normFactTerm, compAsk[1].replace(/^(?:an?|the)\s+/i, "").trim());
|
|
4424
|
+
const obj = factTermVariants(normFactTerm, compAsk[3].replace(/^(?:an?|the)\s+/i, "").trim());
|
|
4425
|
+
const hit = facts.find((f) => f.predicate === compPredicate && subj.has(f.subject) && obj.has(f.object));
|
|
4426
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
4427
|
+
const known = facts.filter((f) => f.predicate === compPredicate && (subj.has(f.subject) || subj.has(f.object)));
|
|
4428
|
+
const shown = known.length ? ` I do know: ${known.slice(0, 3).map(renderFactLine).join("; ")}.` : "";
|
|
4429
|
+
return {
|
|
4430
|
+
text: `I can't confirm that — nothing I remember compares them that way.${shown} If it's true, teach me: "${compAsk[1].trim()} is ${compAsk[2].toLowerCase()} than ${compAsk[3].trim()}".`,
|
|
4431
|
+
replace: true,
|
|
4432
|
+
miss: true,
|
|
4433
|
+
};
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4369
4436
|
// (b0) Derived forward yes/no readers — FORWARD_YESNO_MARKERS, one per
|
|
4370
4437
|
// renderable relation. Runs BEFORE the isa lane because ISA_ASK_RE's lazy
|
|
4371
4438
|
// subject otherwise swallows these shapes whole ("is a wheel part of a
|
|
@@ -6054,9 +6121,13 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
6054
6121
|
const verb = verbRaw.toLowerCase();
|
|
6055
6122
|
if (!GENERAL_VERB_EXCLUDE_RE.test(verb) && !GENERAL_VERB_QUERY_EXCLUDE_RE.test(verb)) {
|
|
6056
6123
|
const subject = subjectRaw.trim();
|
|
6057
|
-
|
|
6124
|
+
// the SAME preposition fold the teach side applies, so "does disk-1
|
|
6125
|
+
// rest on peg-a" looks up mgx:rest-on/"peg-a", matching what
|
|
6126
|
+
// generalVerbTeach actually stored
|
|
6127
|
+
const folded = foldPrepositionIntoPredicate(await generalVerbPredicate(verb), objectRaw);
|
|
6128
|
+
const object = folded.object.replace(/^an?\s+/i, "").trim();
|
|
6058
6129
|
if (subject && object) {
|
|
6059
|
-
const predicate =
|
|
6130
|
+
const predicate = folded.predicate;
|
|
6060
6131
|
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
6061
6132
|
const objVariants = factTermVariants(normFactTerm, object);
|
|
6062
6133
|
const hit = rows
|
|
@@ -6070,11 +6141,15 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
6070
6141
|
const genOpen = q.match(GENERAL_VERB_OPEN_RE);
|
|
6071
6142
|
if (genOpen && !GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(q)) {
|
|
6072
6143
|
const [, subjectRaw, verbRaw] = genOpen;
|
|
6073
|
-
|
|
6144
|
+
// "what does disk-1 rest on" captures "rest on" — split the folded
|
|
6145
|
+
// preposition back off and suffix the minted predicate, mirroring the
|
|
6146
|
+
// teach side's foldPrepositionIntoPredicate
|
|
6147
|
+
const [verb, verbPrep] = verbRaw.toLowerCase().split(/\s+/);
|
|
6074
6148
|
if (!GENERAL_VERB_EXCLUDE_RE.test(verb) && !GENERAL_VERB_QUERY_EXCLUDE_RE.test(verb)) {
|
|
6075
6149
|
const subject = subjectRaw.trim();
|
|
6076
6150
|
if (subject) {
|
|
6077
|
-
|
|
6151
|
+
let predicate = await generalVerbPredicate(verb);
|
|
6152
|
+
if (verbPrep && /^mgx:[a-z]+$/.test(predicate)) predicate = `${predicate}-${verbPrep}`;
|
|
6078
6153
|
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
6079
6154
|
const hits = rankByBiasThenTrust(rows.filter((f) => f.predicate === predicate && subjVariants.has(f.subject)), biasByBundle);
|
|
6080
6155
|
if (hits.length) return { ...renderMany(hits), generalVerbQuery: true };
|
|
@@ -6082,6 +6157,26 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
6082
6157
|
}
|
|
6083
6158
|
}
|
|
6084
6159
|
|
|
6160
|
+
// "what rests on peg-a" / "who sits on the chair" — the reverse-by-OBJECT
|
|
6161
|
+
// mirror of the two general-verb readers above, for taught prepositional
|
|
6162
|
+
// facts. Only ever diverts on a REAL stored hit (the predicate is minted
|
|
6163
|
+
// from the surface verb + folded preposition, so a code question like
|
|
6164
|
+
// "what calls chat.mjs" — no such taught fact — falls through untouched;
|
|
6165
|
+
// this also outranks the spell-corrector's "rests"→"tests" misread, which
|
|
6166
|
+
// otherwise walls this exact phrasing).
|
|
6167
|
+
const genReverse = q.match(/^(?:what|who)\s+([a-z]+)\s+(on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside)\s+(.+?)[?.!\s]*$/i);
|
|
6168
|
+
if (genReverse && !GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(q)) {
|
|
6169
|
+
const [, verbSurface, prep, objectRaw] = genReverse;
|
|
6170
|
+
const verb = verbSurface.toLowerCase();
|
|
6171
|
+
if (!GENERAL_VERB_EXCLUDE_RE.test(verb) && !GENERAL_VERB_QUERY_EXCLUDE_RE.test(verb) && !GENERAL_VERB_NOT_A_VERB_RE.test(verb)) {
|
|
6172
|
+
let predicate = await generalVerbPredicate(verb);
|
|
6173
|
+
if (/^mgx:[a-z]+$/.test(predicate)) predicate = `${predicate}-${prep.toLowerCase()}`;
|
|
6174
|
+
const objVariants = factTermVariants(normFactTerm, objectRaw.replace(/^(?:an?|the)\s+/i, "").trim());
|
|
6175
|
+
const hits = rankByBiasThenTrust(rows.filter((f) => f.predicate === predicate && objVariants.has(f.object)), biasByBundle);
|
|
6176
|
+
if (hits.length) return { ...renderMany(hits), generalVerbQuery: true };
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
|
|
6085
6180
|
// (b) RECALL — "what did i tell you about X": every remembered fact mentioning X.
|
|
6086
6181
|
const told = q.match(TOLD_ABOUT_RE);
|
|
6087
6182
|
if (told) {
|
package/src/finish.mjs
CHANGED
|
@@ -230,14 +230,18 @@ function ruleArticle(segments, rule) {
|
|
|
230
230
|
for (let i = 0; i < out.length; i += 1) {
|
|
231
231
|
const seg = out[i];
|
|
232
232
|
if (seg.type !== "prose") continue;
|
|
233
|
-
// (a) in-span "a artifact" / "an module" (case-insensitive; case preserved)
|
|
234
|
-
|
|
233
|
+
// (a) in-span "a artifact" / "an module" (case-insensitive; case preserved).
|
|
234
|
+
// A hyphen before the candidate article means it's the tail of a
|
|
235
|
+
// hyphenated NAME ("peg-a", "option-a"), not an article — rewriting it
|
|
236
|
+
// corrupts user-quoted data ("peg-a\nUtterance" read as "a Utterance").
|
|
237
|
+
seg.text = seg.text.replace(/(^|[^\w-])(a|an)(\s+)([A-Za-z][\w-]*)/gi, (m, pre, art, sp, word) => {
|
|
235
238
|
const vowel = beginsWithVowelSound(word, rule);
|
|
236
239
|
if (vowel === null) return m;
|
|
237
|
-
return matchCase(art, vowel ? "an" : "a") + sp + word;
|
|
240
|
+
return pre + matchCase(art, vowel ? "an" : "a") + sp + word;
|
|
238
241
|
});
|
|
239
242
|
// (b) boundary: prose ends "…a " / "…an ", next span carries the word
|
|
240
|
-
|
|
243
|
+
// (same hyphenated-name guard as (a))
|
|
244
|
+
const bm = seg.text.match(/(^|[^\w-])(a|an)(\s+)$/i);
|
|
241
245
|
if (bm) {
|
|
242
246
|
const next = out[i + 1];
|
|
243
247
|
const word = next && typeof next.text === "string" ? leadingWord(next.text) : "";
|