@polycode-projects/the-mechanical-code-talker 1.0.6 → 1.0.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.
package/README.md CHANGED
@@ -17,7 +17,9 @@ tmct> /callers checkout
17
17
  tmct> /exit
18
18
  ```
19
19
 
20
- Home page: https://polycode-projects.gitlab.io/the-mechanical-code-talker/
20
+ **[Try it live in your browser →](https://polycode-projects.gitlab.io/the-mechanical-code-talker/)**
21
+ — a real, interactive chat demo running client-side: your browser runs the
22
+ actual query engine against a small example codebase, no server, no install.
21
23
 
22
24
  ## How it interprets you
23
25
 
package/ROADMAP.md CHANGED
@@ -12,87 +12,103 @@ mapped into them (item numbers retained for traceability); the seven sketches
12
12
  formerly held in `code-talker-ideas.txt` are folded into items 8–11 below and
13
13
  the file has been deleted.
14
14
 
15
- ## Where we are now (2026-07-08)
15
+ ## Where we are now (2026-07-09)
16
16
 
17
- `npm test` green (**1055**). **v0.9.6, pushed** (0.8.2 → 0.9.6 across this session — see
18
- `HANDOVER.md` for the exact release chain).
19
-
20
- ### Doing right now (5 background clusters, all worktree-isolated, merged sequentially as each lands)
21
-
22
- - **Cluster A** — Bug 6 (scoped-listing false-empty) + Bug 7 (modal-in-fuzzy-cascade) + the
23
- chat-feel residual pair (items 1-2 of the trio's siblings) + `edgesOfKind` memoization +
24
- ontology numeric-vocabulary declaration.
25
- - **Cluster B** — Bug 8 (goal-reasoner confident-wrong on off-domain requests) + refreshing
26
- `PLAN_CAPABILITY_ROUTER.md`'s stale stage-5 status text.
27
- - **Cluster C** — `PLAN_CHAT_FEEL.md` item 6, the pronoun/temporal/discourse-count trio
28
- (measured red sets from an earlier advisor tick; re-measured against the current graded pool
29
- before any fix).
30
- - ~~**Cluster D** — `PLAN_CODE.md` Track 1~~ **shipped** (see "Now" below). Tracks 2-4 remain
31
- unsigned-off and untouched.
32
- - **Cluster E** — `PLAN_INFERENCE_TESTING.md` Stage 1 (`cax-sco` rule) + Stage 2 (proof-chain
33
- receipts) in `src/syllogise.mjs`, targeting the measured chat-A2 50% ceiling.
34
-
35
- Also in flight: playtest sprint rounds 4-8 (3/3 rounds so far shipped a real fix), the strategy
36
- advisor (restarted, 5-min re-arm).
17
+ `npm test` green (**1245**). **v1.0.7, pushed** (0.9.111.0.0 → 1.0.7 across this session —
18
+ see `HANDOVER.md` for the exact release chain and file:line detail).
37
19
 
38
20
  ### Now: shipped this session
39
21
 
40
- - **5 bugs fixed.** Recall half-match replay + staple-onto-wall; 4 wall-kindness gaps (orientation
41
- blurb repeat, "thanks, `<clause>`" walls, a reexports vocabulary leak, `/describe` not surfacing
42
- taught facts); grain-aware entity resolution (fixes the fuzzy-entity false-empty and a
43
- function/module coverage contradiction); module-grain overview gap. Full list in `HANDOVER.md`.
44
- - **A new feature: predicate-based "find" queries.** "find me the payment class" now works:
45
- type-filtered, fuzzy property-surface matching, with a narrow-then-broaden inheritance-aware
46
- cascade and a boolean-fold generalization for compositional predicate queries. Design:
47
- `archive/PLAN_PREDICATE_QUERIES.md`.
48
- - **Two research tracks landed as code.** The ontology plan's two inert synonym resources
49
- (ConceptNet synonym/similar-to rows, phrasebook synonym families) are now wired into query-time
50
- matching, and the disjointness premise set plus numeric vocabulary grew
51
- (`PLAN_ontology-hierarchies.md`). The advanced-grammar plan's closed-frame subordination and
52
- conditional support, plus presupposition honest-nudges, landed (`PLAN_ADVANCED_GRAMMAR.md`); the
53
- graded pool grew 925 to 1075 cases across 6 new construction families (`chatbench/GRADED.md`).
54
- - **A new benchmark: infbench.** A 6-band classical-logic ladder (INF-A1..C2), mechanically
55
- generated, first baseline in `INFBENCH_0.8.2.md`. Kernel A1/A2 clean; chat A1 100%, A2 exactly
56
- 50% (the cax-sco gap, now measured). The ladder gates at B1 (33%) because the disjointness proof
57
- rule doesn't exist yet.
58
- - **A new demo** (`demo/agentic-loop-demo.mjs`) shows the zero-LLM deduce-plan-execute-compose loop
59
- end to end, and surfaced a real goal-reasoner honesty gap (Bug 8, below).
60
- - **A session-loop fix.** A throwing turn no longer aborts a piped/non-interactive session.
61
- - **4 new plan docs.** `PLAN_ontology-hierarchies.md`, `PLAN_INFERENCE_TESTING.md` (revised so
62
- infbench generation is mechanical, not hand-authored), `archive/PLAN_PREDICATE_QUERIES.md`, and
63
- `PLAN_CODE.md` (new, program synthesis over tmct's closed DSLs, gated on explicit operator
64
- sign-off per track Track 1 signed off, see "Doing" above).
65
- - **`PLAN_TMCT_ECOSYSTEM_INTEGRATION.md`**, landed a code-grounded tmct/bedrock-meter/marginalia
66
- integration plan. Corrects a standing wrong assumption (the `/v1/messages` shim was believed
67
- unmerged; it's been on `main` since 0.8.0) and confirms seonix has already fully migrated onto
68
- tmct as a real dependency.
69
- - **A `/narrate` debug mode.** Opt-in (`/narrate on`, `--narrate`, `TMCT_NARRATE=1`) per-turn
70
- decision narrative which lane/pattern matched, results + sources, a deterministic goal
71
- deduction built for playtest-sprint debugging; zero cost when off (byte-identical regression
72
- guard).
22
+ - **1.0.0 the first-run chat experience, rewritten.** The original trigger: a brand-new
23
+ `npm install` + bare `tmct chat` led with a "no code graph loaded" apology for *any* input,
24
+ including plain greetings and identity questions, even though the seeded ontology/lexicon
25
+ could already answer them. Root-caused to a 0.6.0-era design that over-applied its own honest
26
+ empty-graph orientation. Fixed: identity/capability-led responses (`I'm tmct ...` before any
27
+ caveat), a real self-description + a distinct "no LLM involved" answer for the identity/AI-ID
28
+ family, provably-correct "try this" examples (a `vocabExampleHint` that only ever offers a term
29
+ confirmed to resolve in the session's actual seed state — the prior hint could silently lie
30
+ under `TMCT_NO_SEED=1`), and substantially broadened conversational recognition (dialect,
31
+ register, slang, elongation, a bounded-fuzzy-match typo layer) all via curated closed-set
32
+ additions, not a generalized grammar rule, per the project's standing preference.
33
+ - **The `SKILL_CHAT_PLAYTEST.md` dialogue-flow loop, run for real across 5 tiers.** Tier 0
34
+ (bootstrap/identity), Tier 1 (single touch + one drill-down), Tier 2 (drill-down chains with
35
+ anaphora), and Tier 4 (compositional & comparative) each closed in one pass; **Tier 3
36
+ (cross-concept & relation touches) took 7 passes** cycles 3-9 progressively found and fixed a
37
+ recurring `resolveObject` substring-match weakness (a missing minimum-length floor let short
38
+ staccato connectives like "and"/"it" silently hijack the conversation's focus, producing
39
+ confidently WRONG answers on a *later* turn while looking honest) — patched three times
40
+ point-by-point before cycle 9 found and fixed the actual root cause in one place. Tier 5/6 not
41
+ yet started. The skill doc itself gained two new rules from real incidents this run: always
42
+ `mktemp -d` + exact-path cleanup for scratch fixtures (a wildcard `rm -rf /tmp/pt-*` cleanup
43
+ step got flagged by the harness's own safety policy — no damage, but a real cross-agent risk),
44
+ and never `chat --repo` the committed example fixture directly (dirties a checked-in file).
45
+ - **A live, client-side chat demo on the GitLab Pages homepage.** Not a scripted/pre-recorded
46
+ fake the real `src/ask.mjs` query engine runs in the visitor's browser (wink-nlp loaded from
47
+ `esm.sh`, an import-map shim working around 3 leaf files' Node-only static imports, zero
48
+ changes needed to the engine itself since it was already browser-clean pure JS). Boots with a
49
+ banner, replays a few real pre-verified Q&A turns as "history", asks one randomized (or
50
+ `?q=`-primed) question live, and — as of this session's last addition — has a genuine
51
+ interactive input box so a visitor can type their own questions and get real computed answers.
52
+ `?compact=1` for a minimal primed-link view; `window.tmctAnswer`/`tmctAsk`/`tmctParseEntities`
53
+ exposed for headless/Playwright consumers (curl/jq can never see it no backend on GitLab
54
+ Pages, JS never executes for a plain HTTP client — documented plainly, not oversold).
55
+ - **Operator-found bugs, fixed as they were found by hand-testing the shipped CLI**: relation-
56
+ specific vocabulary filtering ("what is a tree used for" was dumping every known relation
57
+ instead of filtering to UsedFor); a teach-lane "did you mean" suggestion that could echo back
58
+ the user's own input byte-identical (a missing a/an agreement check); out-of-domain small talk
59
+ ("what time is it") hitting the raw grammar wall instead of an honest nudge; a pronoun-subject
60
+ teach-lane gap that could silently store a bogus fact ("he is a module"); a closed-set
61
+ existence-question recognizer ("is there a class called X anywhere" was silently mis-answering
62
+ a *different* question — a relationship check — as if it were a verified existence negative);
63
+ "what else is X" repeating the primary definition verbatim instead of surfacing more; bare
64
+ "what is X" (no article) having no fact-lookup route at all, including for a fact the user had
65
+ *just* taught; and general verb-to-predicate teaching ("remember margo eats ribs" mints its own
66
+ predicate now, not just the closed is/has/are set — "has a" interoperates with the existing
67
+ ConceptNet-sourced `mgx:hasA` data).
68
+ - **New-term teaching + quantifiers.** "redis is a cache" (a genuinely new term, not previously
69
+ in the closed ACE lexicon) is now teachable via a write-side-only fix — the read path already
70
+ worked generically over any subject string, including the existing 2-hop transitive `IsA` proof
71
+ chase. Plus four new phrasings ("some/a few Xs are Ys", "your X is a Y", bare "X is Y" as a
72
+ property assertion) and a stored-quantifier recall ("how many Xs are Ys" → "A few.").
73
+ - **An always-on, short "Goal (inferred): ..." line** on every real structural/vocabulary answer
74
+ — distinct from the pre-existing opt-in `/narrate` full-trace mode, which stays as the deeper
75
+ debug tool. Two correctness bugs in the goal-deduction hook itself got fixed along the way
76
+ (a confidently wrong goal shown on failed teach attempts; a missing goal on relation-force
77
+ answers that resolve via a different path than the normal parse).
78
+ - **Seonix's 17-round dogfooding backlog, triaged and Batch 1 shipped.** A sibling project
79
+ (seonix, consuming tmct as a real dependency) ran extensive dogfooding against both a synthetic
80
+ self-index and a real 27,929-module production estate, relaying findings via the inter-session
81
+ inbox. Triaged into 5 priority batches (4 items were already fixed by intervening work); Batch 1
82
+ (existence-query correctness) shipped. Batches 2-5 queued — see "Next" below.
83
+ - **The version-bump policy, set then revised.** Tried "bump immediately after every push, hold
84
+ locally until the next batch" (to keep the published npm version always matching the last
85
+ pushed commit); reverted after it produced confusing "referencing a version that doesn't exist
86
+ yet" noise mid-session. Current policy: bump only at the moment of actually pushing, as part of
87
+ that same push. Recorded in `CLAUDE.md`.
73
88
 
74
89
  ### Next: the open follow-ups
75
90
 
76
- In priority order (full detail and measured targets in `HANDOVER.md`):
77
-
78
- 1. **Bug 8 FIXED.** A global-mode domain gate now requires the request itself (not just the
79
- caller's declared toolset) to parse as a query about the deduced goal's domain, reusing
80
- `ask.mjs`'s own grammar. Zero-hallucination baseline unchanged; independently re-verified.
81
- 2. **Bug 6.** Scoped listing false-empty: "list modules in `<pkg>`" returns empty though the
82
- unscoped lister shows matches. Found dogfooding a 191k-entity monorepo graph.
83
- 3. **Bug 7.** A modal auxiliary ("should") survives the fuzzy-correction cascade and misreads as
84
- "hold". The diagnosed fix is adding modals to `STOPWORDS`.
85
- 4. ~~`PLAN_TMCT_ECOSYSTEM_INTEGRATION.md`~~ **shipped**see "Now" above.
86
- 5. ~~`PLAN_CODE.md` Track 1 sign-off~~ **signed off and shipped** see "Now" above.
87
- 6. **Still open**: the function-grain forward-shape gap (Bug C+D's grain resolution doesn't
88
- cover the "forward" traversal shape), 8 remaining temporal red ids (each a distinct small
89
- grammar gap), the un-flagged `C1:presupposition`/`C2:garden-path` regression, the
90
- `edgesOfKind` by-subject/by-object endpoint indices (memoization itself is done). Full
91
- detail in `HANDOVER.md`.
92
- 7. **`SKILL_PLAYTEST_SPRINT.md`, in progress.** A capped, delegated, chained playtest loop (each
93
- round a background chat session against `examples/mini-webapp`, appraised and fixed+shipped
94
- live). Rounds 1-3 shipped 3 real fixes (0.9.3-0.9.5); cap raised from 3 to 8 rounds mid-run;
95
- continuing.
91
+ 1. **Tier 5 (teach + recall + reasoning in dialogue) and Tier 6 (the messy real user)** — the
92
+ remaining two rungs of the dialogue-flow ladder, not yet run. Tier 5's territory substantially
93
+ overlaps functionality shipped this session (new-term teaching, quantifier recall) but hasn't
94
+ been run through the playtest loop's own dead-end-hunting discipline yet.
95
+ 2. **Seonix Batches 2-5** (full detail relayed via the inbox, triage summary in this session's
96
+ history): Batch 2 "what is X" vs "what is a X" grammar-level minimal pair (the chat-layer
97
+ fact-lookup side is now fixed; the underlying `grammar.mjs` T5 template still requires the
98
+ article, left alone deliberately pending a closer look at why), a missing reverse-direction
99
+ "is X a superclass of Y" verb entry, "what is a `<noun phrase>`" swallowing the whole phrase
100
+ instead of extracting the head noun. Batch 3 recurring wall patterns (purpose/identity
101
+ "whats X for/about", temporal qualifiers on Commit queries, onboarding/closing questions read
102
+ as literal search strings). Batch 4/5 lower-priority, including a cross-graph disambiguation-
103
+ candidate-ranking weakness seonix evidenced 5 times independently but that doesn't reproduce on
104
+ tmct's own small fixture graph (needs a larger graph to chase).
105
+ 3. **General verb-to-predicate teaching's natural follow-up**: dedicated direct-question
106
+ recognition ("does margo eat ribs", "what does margo eat") — this session's work covers
107
+ teaching + generic retrieval, not verb-specific query phrasings.
108
+ 4. **The Tier-4 "of X" membership gap**: "public methods of TaskController" returns a genuine,
109
+ receipted empty because the class declares no methods of its own (they're inherited)
110
+ extending membership queries to walk inheritance is a bigger structural change than a routing
111
+ fix, deliberately deferred.
96
112
 
97
113
  ### Later: deferred by design, staged inside each plan
98
114
 
@@ -112,6 +128,18 @@ their tables.
112
128
 
113
129
  ### History — what shipped in earlier releases
114
130
 
131
+ **v0.9.12 → v1.0.7** (this session — the first-run UX rewrite + the 5-tier playtest sweep +
132
+ the live browser demo): see "Where we are now" above for the full narrative; short form —
133
+ 1.0.0 shipped the identity/capability-led onboarding rewrite; 1.0.1-1.0.7 shipped, in order, the
134
+ redis/new-term teaching + quantifier phrasings, the always-on Goal-inference line, a teach-lane
135
+ pronoun-subject guard, three operator-found bugs (used-for filtering, teach-suggestion echo,
136
+ out-of-domain small talk), Tier-0/1/2 playtest passes, Tier-3's 7-pass convergence (ending in a
137
+ root-cause fix for the recurring substring-match focus-corruption bug), the diagnosed Tier-4
138
+ compositional-fold fix, the live in-browser chat demo, Seonix Batch 1 (existence-query
139
+ correctness), Tier 4's own playtest pass, and a final bundle ("what else is X", bare "what is X",
140
+ general verb-to-predicate teaching). `npm test` 1055 → 1245 across the session, every commit
141
+ green. Full commit-level detail in `HANDOVER.md`.
142
+
115
143
  **v0.8.2** (the chat-feel wave + rule-general C2): tier-1 CHATBENCH 334/334 (draw A) + 285/285
116
144
  (draw B), zero regressions; the cycle-1 hard-fail `gq-functions-call-fnalpha` flipped green.
117
145
  Landed recall hygiene, preamble/politeness frames, calls∪callsSymbol + grain/meta fallbacks, the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.0.6",
3
+ "version": "1.0.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/ask.mjs CHANGED
@@ -863,6 +863,18 @@ function parseSuperlative(w, lc, nlp) {
863
863
  for (let i = extIdx; i < lc.length; i += 1) {
864
864
  if (EDGE_NOUN_TO_METRIC[lc[i]]) { metric = EDGE_NOUN_TO_METRIC[lc[i]]; metricNoun = lc[i]; break; }
865
865
  }
866
+ // Fallback: a passive-verb-led phrasing puts the participle metric word BEFORE
867
+ // the extreme, not after ("which function IS CALLED the most" — cf. "which
868
+ // module is MOST imported", the already-supported order where the participle
869
+ // trails "most" and the forward scan above already catches it). Scan backward
870
+ // from just before the extreme, taking the CLOSEST metric word — the natural
871
+ // reading when one exists at all. Only a fallback (never overrides a forward
872
+ // hit), so it can't touch any phrasing that already resolves correctly today.
873
+ if (!metric) {
874
+ for (let i = extIdx - 1; i >= 0; i -= 1) {
875
+ if (EDGE_NOUN_TO_METRIC[lc[i]]) { metric = EDGE_NOUN_TO_METRIC[lc[i]]; metricNoun = lc[i]; break; }
876
+ }
877
+ }
866
878
  const connectivity = lc.includes("connected") || lc.slice(extIdx, extIdx + 2).join(" ") === "most connected"
867
879
  || ["largest", "biggest", "smallest"].includes(lc[extIdx]);
868
880
  if (!metric) {
@@ -1063,15 +1075,48 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
1063
1075
  const { blc } = dropLeadCopula(bw, bw.map((x) => x.toLowerCase()));
1064
1076
  return blc.length && blc.every((x) => QUALIFIERS[x]);
1065
1077
  });
1078
+ // A bare "call X and call Y" / "call X but not Y" chain — verb+verb (or
1079
+ // verb+bare-object), no qualifier, no "that" — is ALSO the compositional shape
1080
+ // when branch0 leads with an explicit verb and every OTHER branch is either (a)
1081
+ // an explicit repeat of that SAME mapped verb kind ("call loadStore and call
1082
+ // saveStore" == calls(loadStore) ∩ calls(saveStore)) or (b) a bare object with NO
1083
+ // verb of its own at all ("call saveStore but not loadStore" — "loadStore" alone
1084
+ // inherits "call" via the SAME ellipsis-borrowing buildPredicateAtoms/the atoms
1085
+ // loop below already do for OR-chains like "importing X or Y"; this only widens
1086
+ // the GATE that lets that borrowing fire for and/but-not too). Deliberately
1087
+ // narrower than "any verb+verb and": the compat-guarded bare case ("which classes
1088
+ // extends Base and couples to logging") gives its SECOND branch its own DIFFERENT
1089
+ // explicit verb (coupled-to, not inherits) and so fails case (a) and isn't bare
1090
+ // for case (b) either — it still has no marker and correctly stays on the legacy
1091
+ // ambiguous-parse path, untouched.
1092
+ const sameVerbBranches = predWords.length > 0 ? splitBoolean(predLc, predWords).branches : [];
1093
+ let sameVerbLed = false;
1094
+ if (sameVerbBranches.length > 1) {
1095
+ const firstBlc = sameVerbBranches[0].map((x) => x.toLowerCase());
1096
+ const firstVh = findPhrase(firstBlc, VERB_TO_KIND);
1097
+ if (firstVh && firstVh.start === 0) {
1098
+ sameVerbLed = sameVerbBranches.slice(1).every((bw) => {
1099
+ const blc = bw.map((x) => x.toLowerCase());
1100
+ const vh = findPhrase(blc, VERB_TO_KIND);
1101
+ return vh && vh.start === 0 ? vh.kind === firstVh.kind : !vh;
1102
+ });
1103
+ }
1104
+ }
1066
1105
  // marker gate — the crux of backward-compat: without one of these, this is not a
1067
1106
  // compositional query and we must NOT hijack it from the existing parser.
1068
- if (!(quals.length || relFlag || membershipLed || gerundLed || boolQualLed)) return null;
1107
+ if (!(quals.length || relFlag || membershipLed || gerundLed || boolQualLed || sameVerbLed)) return null;
1069
1108
 
1070
1109
  // empty predicate → a bare qualified class ("public methods")
1071
1110
  if (!predWords.length) {
1072
1111
  let base = { node: "allOfClass", entityType };
1073
1112
  if (!quals.length) return { node: "miss", reason: "nothing to filter or traverse" };
1074
- return { node: "qualifier", filters: quals, inner: base };
1113
+ // entityType carried on the qualifier node itself too (not just `inner`) a
1114
+ // top-level "qualifier" AST has no dedicated evalComposite case, so it falls to
1115
+ // the generic {compositeKind:"set", entityType: ast.entityType||null} catch-all;
1116
+ // without this, a zero-match qualifier query ("public methods of X" with no
1117
+ // public methods) rendered a bare "nothing in the index matches that." with no
1118
+ // entity-kind receipt, same wall-shaped miss as a genuinely unrecognized query.
1119
+ return { node: "qualifier", filters: quals, inner: base, entityType };
1075
1120
  }
1076
1121
 
1077
1122
  const subjPrefix = noun.placeholder ? "what" : `which ${entWord}`;
@@ -1111,7 +1156,9 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
1111
1156
  } else {
1112
1157
  result = { node: "boolean", entityType, atoms };
1113
1158
  }
1114
- if (quals.length) result = { node: "qualifier", filters: quals, inner: result };
1159
+ // entityType carried on the qualifier wrapper too see the identical comment on
1160
+ // the empty-predicate qualifier node above; same catch-all-miss-receipt fix.
1161
+ if (quals.length) result = { node: "qualifier", filters: quals, inner: result, entityType };
1115
1162
  return result;
1116
1163
  }
1117
1164
 
@@ -1537,7 +1584,8 @@ function evalBoolean(graph, ast, opts) {
1537
1584
  function evalAnaphora(graph, ast, opts) {
1538
1585
  const prev = opts && opts.prev;
1539
1586
  if (!Array.isArray(prev) || !prev.length) return { compositeMiss: true, reason: "no-prev", matches: [] };
1540
- let items = prev.map((id) => graph.byId.get(id)).filter(Boolean);
1587
+ const baseItems = prev.map((id) => graph.byId.get(id)).filter(Boolean);
1588
+ let items = baseItems;
1541
1589
  const f = ast.filter;
1542
1590
  if (f && f.type === "qual") {
1543
1591
  items = items.filter((ind) => f.filters.every((q) => qualHolds(graph, ind, QUALIFIERS[q])));
@@ -1554,7 +1602,14 @@ function evalAnaphora(graph, ast, opts) {
1554
1602
  }
1555
1603
  }
1556
1604
  // a count over a prior set names the entity kind when the survivors share a class.
1557
- const common = items.length && items.every((x) => x.class === items[0].class) ? items[0].class : null;
1605
+ // When the filter narrows a real prior set down to ZERO, fall back to the PRIOR
1606
+ // set's own class (still shared, pre-filter) so the honest-empty render still
1607
+ // names what was checked ("nothing in the index matches that (methods)."
1608
+ // instead of a bare, kind-less "nothing in the index matches that.") — the
1609
+ // filter genuinely found no survivors, but the entity kind it filtered is not
1610
+ // itself unknown, so the miss shouldn't read as if it were.
1611
+ const sameClass = (list) => (list.length && list.every((x) => x.class === list[0].class) ? list[0].class : null);
1612
+ const common = items.length ? sameClass(items) : sameClass(baseItems);
1558
1613
  if (ast.mode === "count") return { compositeKind: "count", count: items.length, entityType: common, matches: [] };
1559
1614
  return { compositeKind: "set", matches: items, entityType: common };
1560
1615
  }
package/src/chat.mjs CHANGED
@@ -1266,6 +1266,12 @@ const HAS_PROPERTY_PREDICATE = "mgx:hasProperty";
1266
1266
  // "some/a few Xs are Ys" shape) stay obviously in that same family rather than
1267
1267
  // re-typing the CURIE string at each call site.
1268
1268
  const SUBCLASS_PREDICATE = "rdfs:subClassOf";
1269
+ // Bug 3 (2026-07-09): the SAME "has a" predicate ConceptNet's own /r/HasA
1270
+ // facts already use (FACT_PREDICATE_PHRASES, conceptnet-map.toml) — named
1271
+ // here too so generalVerbTeach's "has"/"have" special case (below) stays
1272
+ // obviously in that same family, interoperable with corpus HasA data on the
1273
+ // read side, rather than minting a redundant mgx:has.
1274
+ const HAS_A_PREDICATE = "mgx:hasA";
1269
1275
 
1270
1276
  /** "<Name> owns/maintains <X>" — the ownership teach declarative. <Name> is one
1271
1277
  * or two name tokens, <X> one code-ish token (a path, a file, a symbol). The
@@ -1299,7 +1305,7 @@ async function teachFact(memoryDir, sessionId, { subject, predicate, object, qua
1299
1305
  provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
1300
1306
  ...(quantifier ? { quantifier } : {}),
1301
1307
  });
1302
- const phrase = FACT_PREDICATE_PHRASES[predicate] || predicate;
1308
+ const phrase = predicatePhrase(predicate);
1303
1309
  return { text: `noted — remembered: ${s} ${phrase} ${o}`, via: "assert", miss: false };
1304
1310
  } catch {
1305
1311
  return null;
@@ -1398,6 +1404,111 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
1398
1404
  return null; // Y unknown too — decline honestly, never guess
1399
1405
  }
1400
1406
 
1407
+ // ---- BUG 3 (2026-07-09, operator-authorized generalizing — "I don't know
1408
+ // where that ban came from, overturn it. build it."): general verb-to-
1409
+ // predicate teaching. "remember tony has a hat" / "remember margo eats ribs"
1410
+ // used to fall straight through teachLane returning null (not even this
1411
+ // lane's own honest miss text) because the ONLY verbs the lane recognized at
1412
+ // all were is/are (class-membership/property) and owns/maintains
1413
+ // (ownership) — a sentence with any OTHER verb never matched a single
1414
+ // recognizer, and fell to the STRUCTURAL code-graph grammar, which of course
1415
+ // can't resolve an arbitrary proper noun as a code entity (confusing,
1416
+ // wrong-context miss text, and sometimes a confidently WRONG "Goal
1417
+ // (inferred)" line — runAsk's own teach-lane goal deduction fixes that half).
1418
+ //
1419
+ // RECOGNITION stays exactly as CLOSED as every other frame in this lane:
1420
+ // wrapper-REQUIRED (teachLane only ever calls generalVerbTeach on `wrapped`,
1421
+ // i.e. only inside an already "remember/note/…"-triggered payload — a bare
1422
+ // "tony has a hat" is never silently reified, same discipline
1423
+ // TEACH_PROPERTY_RE already uses), and only a well-formed <subject> <verb>
1424
+ // <object> triple matches AT ALL (point 6 — a missing/unparseable object
1425
+ // still declines honestly, never a guess). What's generalized is ONLY the
1426
+ // PREDICATE a recognized shape maps to, never what counts as a recognized
1427
+ // shape — the same "recognition closed, mapping generalized" split the
1428
+ // operator explicitly authorized over this dispatch's default "prefer
1429
+ // templates" guidance. ----
1430
+
1431
+ /** <subject> (ONE bare word — a name, "tony"/"margo") <verb> (one lowercase
1432
+ * word) <object> (the rest). Deliberately bounded to a SINGLE-TOKEN subject
1433
+ * with no leading determiner — not the lazy/greedy multi-word subject the
1434
+ * is/are frames elsewhere in this lane tolerate. Reasoning (found live while
1435
+ * building this): without real verb-position knowledge, a positional regex
1436
+ * over an ARBITRARY-length subject is genuinely ambiguous — "every
1437
+ * controller is a handler" would just as happily (mis)parse as
1438
+ * subject="every", verb="controller", object="is a handler" as it would the
1439
+ * intended reading. Bounding the subject to one bare word removes that
1440
+ * ambiguity for exactly the shape this mechanism targets (a name-like
1441
+ * subject, per the operator's own examples); a determiner/quantifier-led or
1442
+ * multi-word subject simply doesn't match here and honestly declines
1443
+ * (point 6) rather than risk a wrong split — the is/are-specific frames
1444
+ * elsewhere in this lane already own that broader territory. */
1445
+ const GENERAL_VERB_TEACH_RE = /^([\w'-]+)\s+([a-z]+)\s+(.+?)[.!?]*$/i;
1446
+ /** Determiners/quantifiers that make the FIRST token an article, not a real
1447
+ * bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
1448
+ * would otherwise happily bind them as a 1-token subject and misread the
1449
+ * REAL subject's second word as the verb. Declining here hands the sentence
1450
+ * back to the is/are-specific frames above/below (their own territory) or an
1451
+ * honest miss — never a guessed split. */
1452
+ const GENERAL_VERB_DETERMINER_RE = /^(?:every|each|all|some|a|an|the|your|my|our|their|his|her|its)$/i;
1453
+ /** Verbs owned by an earlier, more specific recognizer in this lane — is/are
1454
+ * (class-membership/property, above) and owns/maintains (ownership, above).
1455
+ * generalVerbTeach declines outright on these so it can never race a more
1456
+ * specific frame for the same sentence; a genuine miss on one of THESE verbs
1457
+ * stays that frame's own honest miss, never silently reinterpreted here. */
1458
+ const GENERAL_VERB_EXCLUDE_RE = /^(?:is|are|am|owns|maintains)$/i;
1459
+ /** Whole-payload safety net (defense in depth alongside the single-token
1460
+ * subject bound above): if "is"/"are"/"am"/"owns"/"maintains" appears
1461
+ * ANYWHERE in the sentence — not just at the guessed verb position — this
1462
+ * is territory another frame in this lane already owns (or will, in the
1463
+ * is/are payload block right after this one runs), so generalVerbTeach
1464
+ * stands down entirely rather than risk a positional misread of a longer
1465
+ * copula/ownership sentence it was never meant to parse. */
1466
+ const GENERAL_VERB_ANYWHERE_EXCLUDE_RE = /\b(?:is|are|am|owns|maintains)\b/i;
1467
+
1468
+ /** The predicate a general-verb teach payload's VERB maps to. "has"/"have"
1469
+ * special-cases onto the EXISTING mgx:hasA predicate (point 2) — the same
1470
+ * one ConceptNet's own /r/HasA facts already use (FACT_PREDICATE_PHRASES),
1471
+ * so a taught "X has a Y" fact reads back interoperably with corpus HasA
1472
+ * data, rather than minting a redundant mgx:has. Any OTHER verb mints
1473
+ * mgx:<lemma> (point 3a) — proseLemma, the wink-nlp lemmatiser this
1474
+ * codebase already loads elsewhere (prose-nlp.mjs), canonicalizes "eats"/
1475
+ * "ate"/"eating" alike onto the same mgx:eat predicate; when the optional
1476
+ * wink model isn't installed, proseLemma degrades to null (its own
1477
+ * documented contract) and this falls back to the verb AS TYPED — still a
1478
+ * perfectly storable/retrievable predicate, just not cross-inflection
1479
+ * canonicalized. Never a hand-curated per-verb table entry required. */
1480
+ async function generalVerbPredicate(verb) {
1481
+ const v = String(verb || "").toLowerCase();
1482
+ if (v === "has" || v === "have") return HAS_A_PREDICATE;
1483
+ try {
1484
+ const { proseLemma } = await import("./prose-nlp.mjs");
1485
+ const lemma = proseLemma();
1486
+ return `mgx:${lemma ? lemma(v) : v}`;
1487
+ } catch {
1488
+ return `mgx:${v}`;
1489
+ }
1490
+ }
1491
+
1492
+ /** Recognize + resolve a general-verb teach payload into {subject, predicate,
1493
+ * object}, or null when it doesn't fit the shape / names an excluded verb /
1494
+ * is missing a real subject or object (point 6 — an honest decline, never a
1495
+ * guess). Pure recognition + predicate mapping; the caller (teachLane) does
1496
+ * the actual write via the shared teachFact. */
1497
+ async function generalVerbTeach(payload) {
1498
+ const p = String(payload || "").trim();
1499
+ if (GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(p)) return null; // another frame's territory — stand down
1500
+ const m = p.match(GENERAL_VERB_TEACH_RE);
1501
+ if (!m) return null;
1502
+ const [, subjectRaw, verbRaw, objectRaw] = m;
1503
+ const verb = verbRaw.toLowerCase();
1504
+ if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
1505
+ if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) return null; // not a bare-name subject
1506
+ const subject = subjectRaw.trim();
1507
+ const object = objectRaw.replace(/^an?\s+/i, "").trim();
1508
+ if (!subject || !object) return null; // no well-formed triple — honest decline (point 6)
1509
+ const predicate = await generalVerbPredicate(verb);
1510
+ return { subject, predicate, object };
1511
+ }
1401
1512
 
1402
1513
  /** Sentence forms to try asserting for a teach payload: the payload as-is, and
1403
1514
  * (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
@@ -1448,8 +1559,19 @@ function teachSuggestion(payload) {
1448
1559
  * double as legitimate demonstrative entity references elsewhere in this
1449
1560
  * file (DESCRIBE_PRONOUN_RE, NEGATION_PRONOUN_RE et al.), and a claim about
1450
1561
  * a demonstrated entity ("that is a bug", pointing at something real) is a
1451
- * much closer call than "every you is a womble" — not this bug's territory. */
1452
- const TEACH_PRONOUN_RE = /^(?:every\s+|each\s+|all\s+|some\s+|a few\s+|a\s+|an\s+)?(you|i|it|they|he|she|we)\s+(?:is|are|am)\b/i;
1562
+ * much closer call than "every you is a womble" — not this bug's territory.
1563
+ *
1564
+ * WIDENED (Bug 3, 2026-07-09): the verb group used to be the closed
1565
+ * is/are/am copula set — correct while pronoun subjects could only ever
1566
+ * reach a class-membership/property claim, but Bug 3's generalVerbTeach
1567
+ * (below) opens a SECOND way a pronoun subject can reach the store, via ANY
1568
+ * verb ("remember you has a hat", "remember he eats ribs"). A pronoun is
1569
+ * just as invalid a fact subject under a general verb as it is under "is" —
1570
+ * this is a grammatical category error regardless of the verb — so the verb
1571
+ * slot now matches ANY word, not just the copula three, keeping the guard
1572
+ * ahead of every teach recognizer (copula AND general-verb alike) the same
1573
+ * way it already stood ahead of teachSuggestion/unknownSubjectFallback. */
1574
+ const TEACH_PRONOUN_RE = /^(?:every\s+|each\s+|all\s+|some\s+|a few\s+|a\s+|an\s+)?(you|i|it|they|he|she|we)\s+\S+/i;
1453
1575
 
1454
1576
  async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
1455
1577
  const rawInput = String(query).trim();
@@ -1522,6 +1644,24 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
1522
1644
  }
1523
1645
  }
1524
1646
 
1647
+ // GENERAL VERB-TO-PREDICATE TEACH (Bug 3) — "remember <Subject> <verb>
1648
+ // <Object>" where <verb> is neither is/are (handled below via the ACE/
1649
+ // unknown-subject/property paths) nor owns/maintains (handled above).
1650
+ // Wrapper-REQUIRED (`wrapped`, not `raw`) — see generalVerbTeach's own
1651
+ // docblock for why this keeps recognition exactly as closed as every other
1652
+ // frame in this lane. Tried before the is/are `payload` block below so a
1653
+ // non-copula verb never falls through this function returning null with no
1654
+ // miss text at all (the ORIGINAL bug: "remember tony has a hat" never even
1655
+ // reached this lane's own honest-miss cascade, landing on the structural
1656
+ // grammar's wrong-context wall instead).
1657
+ if (wrapped && memoryDir && !QUESTION_LEAD_RE.test(wrapped)) {
1658
+ const gv = await generalVerbTeach(wrapped);
1659
+ if (gv) {
1660
+ const stored = await teachFact(memoryDir, sessionId, gv);
1661
+ if (stored) return stored;
1662
+ }
1663
+ }
1664
+
1525
1665
  let payload = null;
1526
1666
  if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
1527
1667
  else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw)) payload = raw;
@@ -2241,7 +2381,33 @@ const FACT_PREDICATE_PHRASES = {
2241
2381
  "mgx:hasPrerequisite": "requires",
2242
2382
  "mgx:ownedBy": "is owned by", // the teach lane's ownership frame ("Priya owns tasks.mjs")
2243
2383
  };
2244
- const factPhrase = (f) => `${f.subject} ${FACT_PREDICATE_PHRASES[f.predicate] || f.predicate} ${f.object}`;
2384
+
2385
+ /** Bug 3 (2026-07-09) point 3b: the MECHANICAL fallback for a predicate this
2386
+ * table has no curated entry for — specifically generalVerbTeach's minted
2387
+ * "mgx:<lemma>" predicates ("mgx:eat", "mgx:drive", …), which by design have
2388
+ * no per-verb table row (that would be the anti-pattern the operator's
2389
+ * dispatch explicitly called out to avoid). Reconstructs the naive third-
2390
+ * person-singular surface form so "margo mgx:eat ribs" still renders as the
2391
+ * natural "margo eats ribs" — the mechanical INVERSE of singularizeSurface's
2392
+ * own naive -s/-es/-ies fold used elsewhere in this file, same accepted-
2393
+ * limitation trade (no real morphology; a handful of doubly-irregular verbs
2394
+ * render slightly off but never wrong-MEANING). "has"/"have" never reach
2395
+ * this fallback — generalVerbPredicate special-cases them onto the CURATED
2396
+ * mgx:hasA entry above before a predicate is ever minted. Any OTHER unknown
2397
+ * predicate (not the "mgx:<lemma>" shape — e.g. a stray/foreign CURIE) still
2398
+ * renders verbatim, unchanged from before this fix. */
2399
+ function thirdPersonSingularSurface(lemma) {
2400
+ const w = String(lemma || "");
2401
+ if (/[a-z]y$/i.test(w) && !/[aeiou]y$/i.test(w)) return `${w.slice(0, -1)}ies`;
2402
+ if (/(?:s|x|z|ch|sh|o)$/i.test(w)) return `${w}es`;
2403
+ return `${w}s`;
2404
+ }
2405
+ function predicatePhrase(predicate) {
2406
+ if (FACT_PREDICATE_PHRASES[predicate]) return FACT_PREDICATE_PHRASES[predicate];
2407
+ const m = /^mgx:([a-z]+)$/i.exec(String(predicate || ""));
2408
+ return m ? thirdPersonSingularSurface(m[1]) : predicate;
2409
+ }
2410
+ const factPhrase = (f) => `${f.subject} ${predicatePhrase(f.predicate)} ${f.object}`;
2245
2411
 
2246
2412
  // ---- BUG 1 fix (2026-07-08): "what is a tree used for" filters to JUST the
2247
2413
  // UsedFor facts, instead of grammar.mjs's meta-whatis template's lazy tail
@@ -2458,10 +2624,12 @@ async function factAnswer(memoryDir, query, envelope, miss) {
2458
2624
  // alongside the schema-docs answer) and misses (facts answer alone) alike.
2459
2625
  // When the engine produced NO parse at all (the empty-bootstrap graph
2460
2626
  // short-circuits before parsing), the meta FORM is recognized directly on a
2461
- // miss — same required-article discipline as the grammar's own T5 template.
2627
+ // miss — via BARE_WHATIS_RE (chat.mjs's own fact-lookup discipline, article
2628
+ // OPTIONAL — see that regex's docblock for why this is safe to loosen here
2629
+ // even though the structural grammar's T5 keeps the article mandatory).
2462
2630
  let metaTerm = envelope?.parsed?.shape === "meta" ? envelope.parsed.object : null;
2463
2631
  if (!metaTerm && miss && !envelope?.parsed) {
2464
- const m = q.match(/^what\s+(?:is|are)\s+an?\s+(.+?)[?.!\s]*$/i)
2632
+ const m = q.match(BARE_WHATIS_RE)
2465
2633
  || q.match(/^what\s+(?:does|do)\s+(.+?)\s+means?[?.!\s]*$/i);
2466
2634
  if (m) metaTerm = m[1];
2467
2635
  }
@@ -2527,6 +2695,83 @@ async function factAnswer(memoryDir, query, envelope, miss) {
2527
2695
  return null;
2528
2696
  }
2529
2697
 
2698
+ // ---- BUG 1 fix (2026-07-09): "what else is X" repeated the SAME primary
2699
+ // definition sentence verbatim, byte-identical to a plain "what is X" turn
2700
+ // right before it. Root cause: "what else is a function" is NOT itself a
2701
+ // recognized shape anywhere in this file — ask()'s own relaxation cascade
2702
+ // (relaxParse, ask.mjs: NOISE-STRIP then DROP-UNMATCHED) quietly treats
2703
+ // "else" as an unmatched leftover token once the anchored grammar misses the
2704
+ // sentence as typed, drops it, and re-parses the survivor as the ORDINARY
2705
+ // "what is a function" meta shape — a real, non-miss answer, so relaxParse
2706
+ // happily accepts it. By the time curatedDefinitionAnswer/factAnswer see the
2707
+ // query, "else" is already gone and there is nothing left to distinguish a
2708
+ // follow-up asking for MORE from the original question. whatElseAnswer is
2709
+ // recognized FIRST, off the RAW query text (never the relaxed envelope), so
2710
+ // it always gets first look regardless of what the ask engine's own parse
2711
+ // collapsed the sentence to. ----
2712
+
2713
+ /** "what else is/are X" / "what else about X" / "what else do you know about
2714
+ * X" — the follow-up shape asking for information BEYOND whatever the
2715
+ * primary answer already said. Two separate anchors (not one alternation)
2716
+ * because the "is/are" copula form and the "about" form take the article
2717
+ * differently ("what else is a function" vs "what else about the cache").
2718
+ * The negative lookahead on the "is/are" form excludes "what else is
2719
+ * in/inside X" — that's a DIFFERENT, already-working feature (normalize.mjs
2720
+ * PHRASING_FRAMES rewrites it to "what does X contain", a members-of-class
2721
+ * query, tested by chatflow-tier1-single-touch.test.mjs); without this
2722
+ * exclusion this lane's own raw-text-first priority (it runs BEFORE ask()'s
2723
+ * pipeline even gets a look) would wrongly swallow that idiom as a
2724
+ * vocabulary-term lookup for the literal term "in X". */
2725
+ const WHAT_ELSE_IS_RE = /^what\s+else\s+(?:is|are)\s+(?!in\b|inside\b)(?:an?\s+)?(.+?)[?.!\s]*$/i;
2726
+ const WHAT_ELSE_ABOUT_RE = /^what\s+else\s+(?:do\s+you\s+know\s+)?about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
2727
+
2728
+ /** "what else is X" — surface remembered facts about X BEYOND the primary
2729
+ * curated (corpus/seon) prose definition, which is itself never a Facts row
2730
+ * (it comes from a separate prose file, seonDefinitions() — see
2731
+ * curatedDefinitionAnswer) — so every subject-side fact this returns is
2732
+ * genuinely additional information, never a repeat of the definition
2733
+ * sentence. Reuses factAnswer's own subject-scan machinery (memoryFacts +
2734
+ * factTermVariants + renderFactLine + the SAME FACT_ANSWER_CAP/'more'-paging
2735
+ * convention as factAnswer/factReadBack), just filtered/framed differently.
2736
+ *
2737
+ * Honest "nothing more" fallback (never a spurious repeat) in TWO cases: (a)
2738
+ * the term carries no facts at all — there is nothing to add beyond the
2739
+ * definition; (b) every fact line this would show ALREADY appears verbatim
2740
+ * in the immediately-preceding turn's answer (`last.answer`) — meaning the
2741
+ * primary answer was itself an exhaustive fact listing (via:"fact", not a
2742
+ * curated prose definition), so "what else" truly has nothing new to say.
2743
+ * That second check reuses this codebase's own established repeat-detection
2744
+ * discipline (comparing rendered lines against `last.answer` bytes — see
2745
+ * ORIENTATION_REPEAT_ONELINER/WALL_REPEAT_ONELINER for the same pattern). */
2746
+ async function whatElseAnswer(memoryDir, query, last) {
2747
+ if (!memoryDir) return null;
2748
+ const q = String(query).trim();
2749
+ const m = q.match(WHAT_ELSE_IS_RE) || q.match(WHAT_ELSE_ABOUT_RE);
2750
+ if (!m) return null;
2751
+ const term = m[1].trim();
2752
+ if (!term) return null;
2753
+ let normFactTerm;
2754
+ try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
2755
+ const variants = factTermVariants(normFactTerm, term);
2756
+ const hits = (await memoryFacts(memoryDir)).filter((f) => variants.has(f.subject));
2757
+ const nothingMore = {
2758
+ text: `That's everything I know about "${term}" — /memory to see the full picture.`,
2759
+ replace: true,
2760
+ };
2761
+ if (!hits.length) return nothingMore;
2762
+ const lines = hits.map(renderFactLine);
2763
+ const prevAnswer = String(last?.answer || "");
2764
+ if (lines.every((l) => prevAnswer.includes(l))) return nothingMore;
2765
+ const shown = lines.slice(0, FACT_ANSWER_CAP);
2766
+ const rest = lines.slice(FACT_ANSWER_CAP);
2767
+ const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
2768
+ return {
2769
+ text: `Beyond that, here's what else I know about "${term}":\n${shown.join("\n")}${extra}`,
2770
+ replace: true,
2771
+ ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}),
2772
+ };
2773
+ }
2774
+
2530
2775
  /** Ontology plan tracks (a)+(b) (PLAN_ontology-hierarchies.md §3): a LAST-
2531
2776
  * RESORT query-time synonym expansion for a "what is a X"-shaped term with NO
2532
2777
  * direct facts. Deliberately run where the caller runs it (runAsk, after
@@ -3058,13 +3303,34 @@ function relationDefinitions() {
3058
3303
  return seonRelsPromise;
3059
3304
  }
3060
3305
 
3061
- /** The meta term a "what is a X" / "what does X mean" / "define X" question asks
3062
- * about — from the parse when present, else recognized directly (same required-
3063
- * article discipline as the grammar's T5). Null when the line isn't such a form. */
3306
+ /** BUG 2 fix (2026-07-09): "what is a/an <term>" with the article made OPTIONAL,
3307
+ * for the FACT-LOOKUP path only (metaTermOf/factAnswer's own bare-form fallback)
3308
+ * NOT grammar.mjs's structural T5 template, which keeps its article MANDATORY
3309
+ * on purpose (a bare "what is <anything>" would also swallow "what is the
3310
+ * meaning of this codebase", an existing, deliberately honest grammar-miss
3311
+ * regression — test/ask.test.mjs pins it null; see T5's own docblock). That
3312
+ * collision risk is a STRUCTURAL-PARSE concern (T5's tail becomes the literal
3313
+ * graph-query object); it doesn't apply here: this regex only extracts a
3314
+ * SUBJECT STRING to look up against the memory Facts store / curated lexicon —
3315
+ * a miss (no fact, no lexicon entry) is silently absorbed by the caller and
3316
+ * falls through to the ordinary honest-miss cascade, exactly like today's
3317
+ * mandatory-article miss does. Root cause this fixes: "what is john" (no
3318
+ * article) never matched the old mandatory-article regex at all, so a freshly
3319
+ * taught "john rdfs:subClassOf function" fact was invisible to "what is john"
3320
+ * even though "what is a john" (or "what is john used for") would have found
3321
+ * it — the fact-lookup path is a low-collision subject lookup, not a structural
3322
+ * parse, so loosening it here is safe. */
3323
+ const BARE_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
3324
+
3325
+ /** The meta term a "what is a X" / "what is X" / "what does X mean" / "define X"
3326
+ * question asks about — from the parse when present, else recognized directly
3327
+ * via BARE_WHATIS_RE (article optional — see its own docblock for why that's
3328
+ * safe here even though the grammar's own T5 keeps the article mandatory).
3329
+ * Null when the line isn't such a form. */
3064
3330
  function metaTermOf(query, envelope) {
3065
3331
  if (envelope?.parsed?.shape === "meta" && envelope.parsed.object) return envelope.parsed.object;
3066
3332
  const q = String(query).trim();
3067
- const m = q.match(/^what\s+(?:is|are)\s+an?\s+(.+?)[?.!\s]*$/i)
3333
+ const m = q.match(BARE_WHATIS_RE)
3068
3334
  || q.match(/^what\s+(?:does|do)\s+(?:an?\s+)?(.+?)\s+means?[?.!\s]*$/i)
3069
3335
  || q.match(/^define\s+(?:an?\s+)?(.+?)[?.!\s]*$/i);
3070
3336
  return m ? m[1].trim() : null;
@@ -3603,6 +3869,27 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
3603
3869
  // facts/recall (a fact EXTENDS a non-miss schema hit too — NOT miss-gated),
3604
3870
  // (4) TEACH lane (would-miss), (5) the short tailored miss (would-miss).
3605
3871
  let handled = false;
3872
+ // (0) BUG 1 fix (2026-07-09): "what else is X" — recognized off the RAW
3873
+ // query text, before every other lane below (all of which read `envelope`,
3874
+ // already relaxed/reparsed by ask()'s own noise-strip cascade — "else" is
3875
+ // exactly the kind of unmatched token that cascade silently drops, which is
3876
+ // why a plain factAnswer/curatedDefinitionAnswer lookup used to answer
3877
+ // "what else is X" with the byte-identical primary definition, as if
3878
+ // repeating it were new information). via is set to a value NONE of the
3879
+ // downstream `via === "composed"/"fact"/"corpus/seon"` gates match, so a
3880
+ // hit here is final — curatedDefinitionAnswer/conceptForceAnswer never get
3881
+ // a chance to re-answer with the same primary definition afterward.
3882
+ if (memoryDir) {
3883
+ const whatElse = await whatElseAnswer(memoryDir, query, last);
3884
+ if (whatElse) {
3885
+ answer = whatElse.text; via = "fact:what-else"; recordMiss = false; handled = true;
3886
+ if (whatElse.pending) factPending = whatElse.pending;
3887
+ deduced = "surface additional remembered facts beyond the primary definition";
3888
+ note(trace, "lane: (0) WHAT ELSE — \"what else is/about X\" recognized off the raw query, before the relaxation cascade could quietly drop \"else\" and reduce it to a plain \"what is X\"");
3889
+ note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
3890
+ note(trace, `goal: ${deduced} (revised — the raw \"what else\" phrasing was recognized directly, not the relaxed/reparsed envelope)`);
3891
+ }
3892
+ }
3606
3893
  // (1) #2 META/SELF: bare self/session questions ("what do you know", "what is this
3607
3894
  // codebase", "how do i start") → a summary / orientation, answered before the
3608
3895
  // fact-dump readers so "what do you know" gets a summary, not raw facts.
@@ -3702,7 +3989,37 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
3702
3989
  } catch { /* leave false — the ordinary path decides */ }
3703
3990
  }
3704
3991
  }
3705
- if (!handled && miss && !envelope?.parsed && isConversational(query) && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus) {
3992
+ const isConversationalCandidate = !handled && miss && !envelope?.parsed && isConversational(query) && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus;
3993
+ // BUG 2 fix (2026-07-09): "what is X" with NO article ("what is john") is BOTH
3994
+ // conversational-shaped (≤3 words, no code-ish token — isConversational() would
3995
+ // claim it) AND a legitimate bare meta/fact-lookup form (BARE_WHATIS_RE —
3996
+ // metaTermOf's own docblock explains why the article is safe to make optional on
3997
+ // this fact-lookup path specifically). Root cause: grammar.mjs's T5 template
3998
+ // requires the article, so envelope.parsed stays null for the bare form — which
3999
+ // is exactly isConversationalCandidate's own `!envelope?.parsed` gate — so
4000
+ // isConversational used to win the race unconditionally, and a freshly taught
4001
+ // "john is a function" fact became invisible the moment its own subject was
4002
+ // asked back about bare ("what is john" fell to the generic capability-
4003
+ // orientation card, byte-identical to asking about a term tmct had never heard
4004
+ // of). Diverts ONLY when a REAL fact actually resolves for the bare term —
4005
+ // never a speculative reroute: a bare "what is up"/"what is wrong" with nothing
4006
+ // behind it falls straight through to the SAME orientation card as before,
4007
+ // exactly like every other isConversationalCandidate exemption above (each one
4008
+ // guarantees a real answer before it defers, never stranding the turn on a
4009
+ // worse outcome — see isStaccatoPronounNoFocus's own docblock for the same
4010
+ // discipline).
4011
+ let bareMetaHit = null;
4012
+ if (isConversationalCandidate && memoryDir && BARE_WHATIS_RE.test(String(query).trim())) {
4013
+ bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss))
4014
+ ?? (await factReadBack(memoryDir, query, envelope, miss, graph));
4015
+ }
4016
+ if (bareMetaHit) {
4017
+ answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
4018
+ via = "fact"; recordMiss = false; handled = true;
4019
+ if (bareMetaHit.pending) factPending = bareMetaHit.pending;
4020
+ note(trace, "lane: (2b) BARE META FACT — \"what is X\" (no article) resolved to a remembered fact before the conversational catch-all could claim it");
4021
+ note(trace, "source: .tmct/memory Facts (see /memory for provenance per line)");
4022
+ } else if (isConversationalCandidate) {
3706
4023
  // A conversational miss (a greeting, "what can you do", a very short non-code
3707
4024
  // line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
3708
4025
  // Bug B1 (0.8.2 follow-up): this branch carries via:"template" and never
@@ -3861,7 +4178,21 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
3861
4178
  if (taught) {
3862
4179
  answer = taught.text; via = taught.via; recordMiss = taught.miss;
3863
4180
  note(trace, `lane: (4) TEACH — TEACH_RE/OWNS_TEACH_RE/BARE_DECLARATIVE_RE matched, ${taught.miss ? "but the payload could not be stored" : "reified into .tmct/memory"}`);
3864
- note(trace, "goal: teach/remember a new fact");
4181
+ // Goal-line fix (Bug 3 point 4, 2026-07-09): `deduced` was computed WAY
4182
+ // above, straight off envelope.parsed alone (deduceGoalFromParsed) —
4183
+ // the structural grammar has no business parsing a teach-shaped
4184
+ // sentence at all ("remember tony has a hat" isn't a code-graph
4185
+ // question), so whatever it landed on there was either confidently
4186
+ // WRONG (a stray structural template matched part of the sentence and
4187
+ // deduced an unrelated GOAL_BY_KIND entry, e.g. "locate what a
4188
+ // module/class defines") or silently absent (no parse stood). Every
4189
+ // successfully-RECOGNIZED teach attempt (`taught` stood — whether it
4190
+ // went on to STORE or to its own honest teach-miss text) gets the SAME
4191
+ // honest, consistent goal line here instead — the same "revise off the
4192
+ // LANE that actually answered, not the raw structural parse"
4193
+ // discipline the relation-force fix above already uses.
4194
+ deduced = "teach/remember a new fact";
4195
+ note(trace, `goal: ${deduced} (revised — the teach lane recognized this shape where the raw structural parse never should have)`);
3865
4196
  }
3866
4197
  }
3867
4198
  // (4b) #4 AUTHOR lane (0.8.2 WS4) — "who is <Name>", "what did <Name> touch",