@polycode-projects/the-mechanical-code-talker 1.0.9 → 1.2.0

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 (3) hide show
  1. package/ROADMAP.md +2 -1
  2. package/package.json +1 -1
  3. package/src/chat.mjs +206 -14
package/ROADMAP.md CHANGED
@@ -20,7 +20,7 @@ Tiers 0, 1, 2, and 4 each closed in one pass. Tier 3 took 7 passes to track down
20
20
  turned up one genuinely important correctness bug alongside a batch of routing fixes. Full
21
21
  per-cycle detail is in `HANDOVER.md`'s "The dialogue-flow playtest loop" section.
22
22
 
23
- `npm test` is green at **1352** (up from 1258 at the start of this session). v1.0.7 is
23
+ `npm test` is green at **1355** (up from 1258 at the start of this session). v1.0.7 is
24
24
  published (0.9.11 → 1.0.0 → 1.0.7 across an earlier session; the exact release chain and
25
25
  file:line detail are in `HANDOVER.md`). Nothing has pushed since, so the local version sits at
26
26
  1.0.9 per the bump-at-push-time policy.
@@ -34,6 +34,7 @@ Test count across the session's later stretch:
34
34
  | Tier 5 (teach + recall + reasoning in dialogue), 5 cycles | 1307 → 1328 |
35
35
  | Tier 6 (the messy real user), 5 cycles, run alongside a background test-suite health pass | 1328 → 1345 |
36
36
  | Compound-name resolution (multi-word queries to joined-token symbol names) | 1345 → 1352 |
37
+ | Vocabulary-growth mirror fix — known-subject/unknown-object mint (`unknownObjectFallback`), so new terms compound turn over turn | 1352 → 1355 |
37
38
 
38
39
  ### Shipped this session
39
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.0.9",
3
+ "version": "1.2.0",
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
@@ -1474,6 +1474,112 @@ const SOME_A_FEW_RE = /^(some|a few)\s+([\w-]+)\s+are\s+([\w-]+)$/i;
1474
1474
  * the ambiguity a fully free-form multi-word subject would otherwise have. */
1475
1475
  const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:is|are)\s+(?:an?\s+)?([\w-]+)$/i;
1476
1476
 
1477
+ /** ISA-family predicates (mirrors the private ISA_PREDICATES set defined near
1478
+ * memoryFacts, below, at module scope — both are simple top-level consts
1479
+ * evaluated once at load time, so referencing either from a function defined
1480
+ * earlier in this file is safe: no function here actually RUNS until well
1481
+ * after the whole module has finished loading). Named again here, right by
1482
+ * its one caller, so isGroundedByFact reads standalone. */
1483
+ const MINT_ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
1484
+
1485
+ /** Small CLOSED set of generic English root nouns that count as always-
1486
+ * grounded anchor terms for the mint-fallbacks below (operator refinement,
1487
+ * 2026-07-09) — deliberately NOT added to lexicon-core.json itself (that
1488
+ * file stays the curated ~180-word CODE vocabulary; these are ordinary-
1489
+ * English root nouns with no code meaning at all, confirmed absent from it
1490
+ * today). Their only job is to give a user who hits the "both sides
1491
+ * ungrounded" decline (groundingSuggestionMiss, below) an honest, guessable
1492
+ * way in: ground one brand-new term via one of THESE words first ("every
1493
+ * zorp is a thing"), then chain the other new term off the now-grounded one. */
1494
+ const GENERIC_ANCHOR_NOUNS = new Set(["thing", "concept", "object", "entity"]);
1495
+
1496
+ /** Shared fact-groundedness primitive (Feature A mint-extension, point 2):
1497
+ * true when `term` already appears as the SUBJECT or OBJECT of a previously
1498
+ * taught isa-family fact (rdfs:subClassOf/rdf:type) in memory. A term minted
1499
+ * by EITHER mint-fallback below (this session, or an earlier one — this
1500
+ * reads persisted memory, not session-scoped state) is exactly as legitimate
1501
+ * an anchor for a NEW fact as a static lexicon-core.json word — the whole
1502
+ * point of this extension is letting new vocabulary compound turn over turn
1503
+ * ("every cache is a store" mints "store"; "every store is a container" then
1504
+ * needs "store" to read as known even though it's not in the static lexicon
1505
+ * at all). Read-only, reuses memoryFacts' plain read path (existence only —
1506
+ * no trust-ranking needed here) and normFactTerm's own normalization, so a
1507
+ * fact-grounded term matches under the EXACT spelling teachFact itself stored
1508
+ * it under. Failure-tolerated: no memory dir / no match → false, never a
1509
+ * guessed "yes". */
1510
+ async function isGroundedByFact(term, memoryDir) {
1511
+ if (!memoryDir) return false;
1512
+ const raw = String(term ?? "").trim();
1513
+ if (!raw) return false;
1514
+ const { normFactTerm } = await import("./memory/core.mjs");
1515
+ const t = normFactTerm(raw);
1516
+ if (!t) return false;
1517
+ // TAUGHT-only (same discipline factReadBack's own cax-sco/scm-sco proof
1518
+ // chase already uses, above, for the identical reason: the bulk background
1519
+ // corpus band (ConceptNet, trust 0.7, seeded by the thousands on a fresh
1520
+ // repo) mentions ordinary English words like "store"/"container" constantly
1521
+ // — treating THOSE as "grounded" would silently reopen the general lexicon
1522
+ // bypass this whole feature is deliberately narrow to avoid. Only what the
1523
+ // OPERATOR actually taught (or a prior `tmct syllogise` entailment) anchors
1524
+ // a term here. factRows (not memoryFacts) is used specifically because it's
1525
+ // the one read path that carries sourceTypes for this filter.
1526
+ const rows = await factRows(memoryDir);
1527
+ const isTaught = (f) => !f.sourceTypes?.includes("corpus") && !f.sourceTypes?.includes("web");
1528
+ return rows.some((f) => MINT_ISA_PREDICATES.has(f.predicate) && isTaught(f) && (f.subject === t || f.object === t));
1529
+ }
1530
+
1531
+ /** Shared "is this term grounded in ANY sense" aggregate (Feature A mint-
1532
+ * extension, point 2's named shared helper) — a static lexicon word (any
1533
+ * part of speech, via `classify`), a GENERIC_ANCHOR_NOUNS root, OR a term
1534
+ * already anchored by a previously taught isa-family fact (isGroundedByFact,
1535
+ * above). Used by unknownObjectFallback's subject/object groundedness checks
1536
+ * below, where no part-of-speech branching follows — just "known or not".
1537
+ * (unknownSubjectFallback's own object-known check, above/below, stays
1538
+ * narrower and NOUN-specific — see its own comment — so an object that's
1539
+ * merely a known ADJECTIVE doesn't get misrouted into the class/subClassOf
1540
+ * branch instead of the property branch.) */
1541
+ async function isGroundedTerm(term, lex, memoryDir) {
1542
+ const raw = String(term ?? "").trim();
1543
+ if (!raw) return false;
1544
+ if (GENERIC_ANCHOR_NOUNS.has(raw.toLowerCase())) return true;
1545
+ const { classify } = await import("./grammar/lexicon.mjs");
1546
+ if (classify(raw, lex)) return true;
1547
+ return isGroundedByFact(raw, memoryDir);
1548
+ }
1549
+
1550
+ /** The "both sides ungrounded" grounding NUDGE (operator refinement,
1551
+ * 2026-07-09): reuses teachSuggestion's own "compute a hint, APPEND it to
1552
+ * the existing honest-miss message, never replace/silently guess" pattern
1553
+ * (see its docblock, above, and the "did"/"why" append-style construction in
1554
+ * teachLane's own final decline, below) for a DIFFERENT decline case —
1555
+ * rather than mint a relationship between two brand-new terms (a real
1556
+ * fabrication risk, unknownObjectFallback's own explicit safety guard,
1557
+ * below), teachLane's final honest-miss text gets an EXTRA appended nudge
1558
+ * whenever the declined payload fit the "X is/are Y" shape
1559
+ * (UNKNOWN_SUBJECT_RE) but NEITHER side is grounded — an honest, actionable
1560
+ * way in: ground one side via a GENERIC_ANCHOR_NOUNS root first ("every zorp
1561
+ * is a thing"), then chain the other off the now-grounded term. Deliberately
1562
+ * APPENDED rather than a replacement/short-circuit: a "both sides
1563
+ * ungrounded" is/are sentence with a KNOWN subject on one side (e.g. "module
1564
+ * is banana") never reaches this at all (isGroundedTerm(subject) is true, so
1565
+ * the very first return below fires) — that stays unknownObjectFallback's
1566
+ * own mint territory, entirely unaffected here. Returns "" (message
1567
+ * unchanged) whenever the payload doesn't fit the shape, or at least one
1568
+ * side IS already grounded — a DIFFERENT, more specific reason it declined,
1569
+ * where this nudge would be actively unhelpful noise. */
1570
+ async function ungroundedPairHint(payload, lexicon, memoryDir) {
1571
+ if (!memoryDir) return "";
1572
+ const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
1573
+ if (!m) return "";
1574
+ const [, , subjectRaw, objectRaw] = m;
1575
+ const { loadLexicon } = await import("./grammar/lexicon.mjs");
1576
+ const lex = lexicon || loadLexicon();
1577
+ if (await isGroundedTerm(subjectRaw, lex, memoryDir)) return "";
1578
+ if (await isGroundedTerm(objectRaw, lex, memoryDir)) return "";
1579
+ return ` I don't know "${subjectRaw}" or "${objectRaw}" yet. Try grounding one first, e.g. `
1580
+ + `"every ${subjectRaw} is a thing", then "every ${objectRaw} is a ${subjectRaw}".`;
1581
+ }
1582
+
1477
1583
  /** The unknown-SUBJECT direct-write fallback (point 1 + point 2's bare-property
1478
1584
  * extension): tried ONLY after the real ACE grammar (assertTurn) has already
1479
1585
  * had its turn and declined. Declines itself (returns null, never a guess)
@@ -1485,18 +1591,25 @@ const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|a\s+|an\s+)?([\w-]+(?:\s+[
1485
1591
  * - X is actually a KNOWN lexicon word — then the ACE grammar's own miss was
1486
1592
  * a real structural/vocabulary problem elsewhere (e.g. Y itself unknown as
1487
1593
  * the WRONG part of speech), never silently reinterpreted through this
1488
- * narrow exception;
1489
- * - Y resolves as NEITHER a known noun NOR a known adjective — the OBJECT
1490
- * must still be a term tmct actually knows; an unknown Y stays an honest
1491
- * miss (never a guess), exactly like the pre-existing "monkey is an
1492
- * animal" case.
1493
- * Y resolving as a NOUN writes rdfs:subClassOf (mirrors the ACE grammar's own
1494
- * subClassOf/typeAssertion pattern); Y resolving as an ADJECTIVE (and not also
1495
- * a noun) writes mgx:hasProperty (mirrors the wrapped "remember that X is
1496
- * deprecated" property frame reused here for the bare/unwrapped form too,
1497
- * since the free pass is about the SUBJECT, not about the "remember that"
1498
- * wrapper). Only the "every" determiner records a quantifier (point 3: "a"/
1499
- * bare/"your" read as one specific entity, not a class-level generalization). */
1594
+ * narrow exception. (NOTE: this stays a STATIC-lexicon-only check,
1595
+ * deliberately not widened to isGroundedTerm a subject that's grounded
1596
+ * only via a PRIOR taught fact, not the static lexicon, is precisely the
1597
+ * case unknownObjectFallback, below, owns instead.)
1598
+ * - Y resolves as NEITHER a known noun NOR a known adjective NOR a term
1599
+ * already grounded by a prior taught fact / a GENERIC_ANCHOR_NOUNS root
1600
+ * (Feature A mint-extension, point 2 a term minted by
1601
+ * unknownObjectFallback, below, reads exactly as known here as any
1602
+ * lexicon word)the OBJECT must still be a term tmct actually knows;
1603
+ * an unknown Y stays an honest miss (never a guess), exactly like the
1604
+ * pre-existing "monkey is an animal" case.
1605
+ * Y resolving as a NOUN (or fact-/anchor-grounded) writes rdfs:subClassOf
1606
+ * (mirrors the ACE grammar's own subClassOf/typeAssertion pattern); Y
1607
+ * resolving as an ADJECTIVE (and not also a noun) writes mgx:hasProperty
1608
+ * (mirrors the wrapped "remember that X is deprecated" property frame —
1609
+ * reused here for the bare/unwrapped form too, since the free pass is about
1610
+ * the SUBJECT, not about the "remember that" wrapper). Only the "every"
1611
+ * determiner records a quantifier (point 3: "a"/bare/"your" read as one
1612
+ * specific entity, not a class-level generalization). */
1500
1613
  async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }) {
1501
1614
  if (!memoryDir) return null;
1502
1615
  const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
@@ -1507,7 +1620,12 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
1507
1620
  // A known X's own ACE miss is a real miss — never silently reinterpreted here.
1508
1621
  if (classify(subjectRaw, lex)) return null;
1509
1622
  const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
1510
- if (lookupNoun(lex, objectRaw)) {
1623
+ // Point 2 (mint-extension): a PRIOR turn's minted term, or a
1624
+ // GENERIC_ANCHOR_NOUNS root, grounds Y just as legitimately as a static
1625
+ // lexicon noun — both are always treated as class-level (never property),
1626
+ // consistent with unknownObjectFallback (below) always minting a CLASS.
1627
+ if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
1628
+ || (await isGroundedByFact(objectRaw, memoryDir))) {
1511
1629
  return teachFact(memoryDir, sessionId, {
1512
1630
  subject: subjectRaw, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier,
1513
1631
  });
@@ -1522,6 +1640,67 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
1522
1640
  return null; // Y unknown too — decline honestly, never guess
1523
1641
  }
1524
1642
 
1643
+ /** The unknown-OBJECT mint fallback (Feature A, 2026-07-09 operator-authorized
1644
+ * vocabulary-growth extension): the MIRROR of unknownSubjectFallback, above —
1645
+ * same "X is/are Y" payload shape (UNKNOWN_SUBJECT_RE, reused verbatim,
1646
+ * never a second regex for the identical shape), tried as a SIBLING call
1647
+ * right after unknownSubjectFallback in teachLane (below), but firing on the
1648
+ * OPPOSITE asymmetry: SUBJECT already grounded (a real lexicon-core.json
1649
+ * word of ANY part of speech, a GENERIC_ANCHOR_NOUNS root, OR a term a PRIOR
1650
+ * turn already minted via either fallback — isGroundedTerm, shared with this
1651
+ * check) and OBJECT completely ungrounded. Mints the object as a new
1652
+ * class-level concept (rdfs:subClassOf, same predicate/quantifier machinery
1653
+ * teachFact/unknownSubjectFallback already use) so ordinary conversation can
1654
+ * build up new vocabulary turn over turn: "every cache is a store" (subject
1655
+ * "cache" grounded via the static lexicon) mints "store"; a LATER "every
1656
+ * store is a container" then finds "store" grounded via the fact just
1657
+ * minted (not the static lexicon at all) and mints "container" the same way.
1658
+ *
1659
+ * GATED ON A GENUINE UNIVERSAL QUANTIFIER ("every"/"each"/"all" — never bare/
1660
+ * "a"/"an"/"your"): minting a NEW CLASS-LEVEL CONCEPT is inherently a general
1661
+ * claim about a class, the same "every"/bare distinction unknownSubjectFallback's
1662
+ * own docblock already draws (point 3) between a class generalization and a
1663
+ * claim about ONE specific entity. This is load-bearing, not cosmetic: a bare
1664
+ * "module is banana" (a KNOWN lexicon subject, an unrecognized bare object,
1665
+ * NO determiner at all) is a pinned regression — it must stay a plain honest
1666
+ * miss, never silently minted — and a WRAPPED "remember that X is <adjective>"
1667
+ * (also determiner-less at the subject) must keep falling through to
1668
+ * TEACH_PROPERTY_RE's own, more permissive arbitrary-adjective path
1669
+ * unimpeded. Requiring the determiner keeps this fallback's mint exactly as
1670
+ * narrow as the vocabulary-growth feature actually needs (every required
1671
+ * test case in this feature's own spec phrases the mint sentence with
1672
+ * "every"), without swallowing either of those pre-existing shapes.
1673
+ *
1674
+ * The critical safety guard (operator's own stated worry, mirrored from
1675
+ * unknownSubjectFallback's docblock): this must NEVER silently mint when
1676
+ * BOTH sides are ungrounded ("every zorp is a florp" — two brand-new,
1677
+ * never-seen terms with no relation to each other tmct actually knows) —
1678
+ * declines here (null), and teachLane's own final honest-miss text picks up
1679
+ * an appended grounding NUDGE for exactly this case (ungroundedPairHint,
1680
+ * above) — never a silent guess, never a silent hard-swallowed decline
1681
+ * either. Any OTHER decline (subject ungrounded + object grounded — not this
1682
+ * fallback's asymmetry; or both grounded — already known, nothing to mint;
1683
+ * or no genuine universal quantifier) falls through as a plain null,
1684
+ * letting the ordinary teachLane cascade (property teach, then the generic
1685
+ * honest-miss text) continue unaffected. */
1686
+ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon }) {
1687
+ if (!memoryDir) return null;
1688
+ const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
1689
+ if (!m) return null;
1690
+ const [, det, subjectRaw, objectRaw] = m;
1691
+ if (!/^(?:every|each|all)$/i.test((det || "").trim())) return null; // class-level mint needs a real universal quantifier
1692
+ const { loadLexicon } = await import("./grammar/lexicon.mjs");
1693
+ const lex = lexicon || loadLexicon();
1694
+ const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir);
1695
+ if (!subjectGrounded) return null; // ungrounded subject isn't this fallback's asymmetry — never a guessed mint
1696
+ const objectGrounded = await isGroundedTerm(objectRaw, lex, memoryDir);
1697
+ if (objectGrounded) return null; // object already known — nothing to mint
1698
+ const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
1699
+ return teachFact(memoryDir, sessionId, {
1700
+ subject: subjectRaw, predicate: SUBCLASS_PREDICATE, object: objectRaw, quantifier,
1701
+ });
1702
+ }
1703
+
1525
1704
  // ---- BUG 3 (2026-07-09, operator-authorized generalizing — "I don't know
1526
1705
  // where that ban came from, overturn it. build it."): general verb-to-
1527
1706
  // predicate teaching. "remember tony has a hat" / "remember margo eats ribs"
@@ -1922,6 +2101,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
1922
2101
  // the exact narrowing rules (object must still be known, etc.).
1923
2102
  const fallback = await unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon });
1924
2103
  if (fallback) return fallback;
2104
+ // MIRROR mint fallback (Feature A, 2026-07-09 operator-authorized vocabulary-
2105
+ // growth extension): the known-subject/unknown-object asymmetry — tried
2106
+ // right after the unknown-subject case declines, so a subject the STATIC
2107
+ // lexicon (or a prior taught fact) already grounds can mint a brand-new
2108
+ // object term. See unknownObjectFallback's own docblock for the exact
2109
+ // narrowing rules (the "both sides ungrounded" safety guard, etc.).
2110
+ const objectFallback = await unknownObjectFallback(payload, { memoryDir, sessionId, lexicon });
2111
+ if (objectFallback) return objectFallback;
1925
2112
  // PROPERTY teach — "remember/note that <X> is <adjective>": wrapper-REQUIRED
1926
2113
  // (a bare "X is deprecated" is never silently reified), and only after the
1927
2114
  // ACE grammar declined (unknown words / not the membership shape), so a
@@ -1980,9 +2167,14 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
1980
2167
  + "I can only teach facts using tmct's own code-vocabulary nouns (like module, class, function…), "
1981
2168
  + "not arbitrary new terms."
1982
2169
  : "";
2170
+ // Grounding NUDGE (operator refinement, 2026-07-09): APPENDED, never a
2171
+ // replacement, exactly like "did" above — see ungroundedPairHint's own
2172
+ // docblock for why this is scoped to the "both sides ungrounded, fits the
2173
+ // X is/are Y shape" case only.
2174
+ const groundingHint = await ungroundedPairHint(payload, lexicon, memoryDir);
1983
2175
  return {
1984
2176
  text: `I couldn't store that —${why} I remember facts in the shape "every X is a Y", where X and Y are `
1985
- + `words I know.${did} Type /memory to see what I already remember.`,
2177
+ + `words I know.${did}${groundingHint} Type /memory to see what I already remember.`,
1986
2178
  via: "teach-miss", miss: true,
1987
2179
  };
1988
2180
  }