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

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 +72 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.10.7",
3
+ "version": "1.10.8",
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);