@polycode-projects/the-mechanical-code-talker 0.8.0 → 0.8.2

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/ROADMAP.md CHANGED
@@ -12,36 +12,65 @@ 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-06)
16
-
17
- **Built: v0.8.0Phase 11 landed (Phases 0–10 shipped in 0.7.1).** All five Phase-11 tracks are
18
- built, merged, and measured; `npm test` green (**869**). Push/publish held for the operator. Highlights:
19
-
20
- - **Track 1 chat levers (measured).** Pronoun/focus binding, discourse-count anaphora, and C1
21
- temporal-over-relative composition all landed. `CHATBENCH_0.8.0`: tier-1 spine **331→333**, all three
22
- lever families moved their cells (the B1-temporal control did not spill); judged fixed tag
23
- `multi-turn-focus` **1.433→1.9** (the pronoun lever's echo). The judged pooled mean (1.44) is **not
24
- case-comparable** to 0.7.1's 1.488 — the graded draw re-samples 10% of cells each run, so §1 compares
25
- cell-level, not the scalar; the deterministic tier-1 spine is the load-bearing PASS.
26
- - **Track 2 the router (designed DEMONSTRATED).** The Anthropic-compatible `/v1/messages` shim
27
- (`serve` mode), the Stage-0 capability registry (STRIPS/PDDL operators, closed-world default-deny),
28
- and the resolver (S1) + guardrail (S4) + planner (S3, pure-JS POP/HTN + Steel & Ho monitor). Measured:
29
- `AGENTBENCH_0.8.0` (shim-transport floor, 46% completion) `AGENTBENCH_0.8.0_001` (the real router):
30
- **96% completion at 0% hallucination on every rung**, clearing the closed-world ladder to **C1**. The
31
- honest gate the phase was staked on is **met on its hallucination axis (0%)**. **Caveat (stated in the
32
- artifact):** AGENTBENCH grades the correct **call-plan + proof**, *not* the executed composed result,
33
- and B1/B2/C1 are thin rungs (2–3 cases); the C2 case is honestly **refused** (Stage 5, unbuilt).
34
- - **Track 3 — bedrock-meter surface.** `../bedrock-meter` gained a cost-ascending router ladder with the
35
- **tmct `$0` rung at rank 0 below nova-micro**; an end-to-end test meters an in-envelope request at
36
- **£0** and escalates out-of-envelope to nova-micro (that repo: 73 green, unpushed).
37
- - **Track 4 playtest.** Three frozen dialogue-flow transcripts (`test/chatflow-{coverage,history,
38
- architecture}.test.mjs`) from routing dead-ends off the grammar wall.
39
- - **Track 5 research.** `docs/references/planning/` gained Stage-2 intent-frames + Stage-5
40
- goal-reasoner design notes + a BDI/Goal-Driven-Autonomy reference entry.
41
-
42
- **Next:** the C2 goal-reasoner (Stage 5) and Stage-2 imperative intent frames are the designed-but-
43
- unbuilt edge; the tier-4/tier-5 items in Phase LATER remain the long tail. See Phase 11 below and
44
- `HANDOVER.md` for the open follow-ups.
15
+ ## Where we are now (2026-07-07)
16
+
17
+ **Built: v0.8.2the chat-feel wave + rule-general C2 (0.8.1 published; 0.8.2 gated green, push =
18
+ the next operator action).** `npm test` green (**974**). 0.8.2 deepened both axes at once:
19
+
20
+ - **Chat feel (CHATBENCH_0.8.2, deterministic tier):** tier-1 **334/334 (draw A) + 285/285 (draw
21
+ B)**, zero regressions; the cycle-1 standing hard-fail `gq-functions-call-fnalpha` **flipped
22
+ green**. Landed (PLAN_CHAT_FEEL items 1–5, 7, 8): recall hygiene, preamble/politeness frames,
23
+ calls∪callsSymbol + grain/meta fallbacks, the author lane, wall kindness + honest capability
24
+ nudges, teach-lane widening + the taught-class↔inherits bridge, receipt tails prose→detail.
25
+ Plus a live-found scale hotfix (edgesOfKind argument-spread overflow past ~100k edges, from a
26
+ 27,770-module monorepo). **The judged (LLM) re-judge of the touched tags is DEFERRED
27
+ post-release (ship-over-delay)** 0.8.1 judged scores stay the judged record and are stale on
28
+ those tags; see CHATBENCH_0.8.2.
29
+ - **Agent (AGENTBENCH_0.8.2):** ladder 43→56 cases; goal driver **100% plan / 98% result / 0%
30
+ hallucination, all rungs gate-PASS** (one deliberate honest red: `ab-c2-what-to-test`); resolver
31
+ floor clean **A0–C1 100/100** via the new member-filter HTN method + per-member hop (flips
32
+ `ab-c1-widget-methods-calling` in both drivers). **C2 is now rule-general**: a second declared
33
+ goal-rule (`cochange-risk-invariant`) + pure `applicableRules` selection (0→refuse open-world,
34
+ >1→refuse ambiguous; grep-clean of request keywords). Bench-import inversion
35
+ (`src/router/call-validator.mjs` + `set-algebra.mjs`); bounded runner pool (`--concurrency` 8).
36
+
37
+ **Shipped in v0.8.1 (published):** the same demonstrated-vs-designed honesty discipline, summarized:
38
+
39
+ - **AGENTBENCH now grades the executed composed RESULT, not just the call-plan** (retires the headline
40
+ 0.8.0 caveat). Under result-grading the resolver is **97% plan / 91% result / 0% hallucination**; the
41
+ honest gap lands exactly where reasoning is hard (C1, C2).
42
+ - **Stage 5 — the C2 goal-reasoner** (BDI + Goal-Driven Autonomy) genuinely lifts result-completion.
43
+ The honest, **like-for-like** figure is the *driver swap on the identical 39-case ladder*
44
+ (`AGENTBENCH_0.8.1_001`): resolver **85% → goal-reasoner 95% result** (+10pp, purely the driver, 0%
45
+ hallucination). C2 is **cleared for one declared coverage-invariant goal-rule** — real and
46
+ phrasing-robust (validated by held-out phrasings graded blind, no request-string literals), but
47
+ **thinly sampled, not rule-general**; the open-world case is honestly **refused**. *(NB: the
48
+ cross-release "0→83% C2" reads on a grown 6-case C2 ladder whose 4 new cases exercise the
49
+ goal-reasoner — not a like-for-like rate; the same-basket win is one case fixed, 0→50% original C2.)*
50
+ - **Stage 2 — imperative intent frames + ACE reach** (`AGENTBENCH_0.8.1_002`, goal driver): **100% plan
51
+ / 95% result / 0% hallucination**. `tmct_calls` is now **genuinely NL-reachable** via a distinct
52
+ edge-dump frame (not by un-tagging — bidirectional conformance enforces it; `NOT_NL_REACHABLE` is now
53
+ `{}`). ACE is wired **async** into the interpret pipeline so the synchronous CHATBENCH parse spine is
54
+ **byte-identical** — ACE is inert in the shipped product path (`interpret()` is called nowhere in
55
+ chat/ask/server/bin), reach without regression.
56
+ - **Chat surface (CHATBENCH_0.8.1):** quick wins (singular "what is a test", friendly commit-author
57
+ refs, "No tests cover X" honest-empty, the chatbench discourse-count flake root-caused + scrubbed) +
58
+ two playtests (coverage-survey + authorship routing; **6 frozen `chatflow-*` transcripts** total).
59
+ Joint deterministic CHATBENCH (both new frame tables together): **no tier-1 regression vs 0.7.1.**
60
+ Judged tags touched by the text changes were re-judged (not blanket-reused) — see `CHATBENCH_0.8.1`.
61
+
62
+ **Shipped earlier in v0.8.0 (published):** all five Phase-11 tracks — the `/v1/messages` shim + Stage-0
63
+ registry + resolver/guardrail/planner (`AGENTBENCH_0.8.0_001`: 96% *plan* completion, 0% hallucination,
64
+ closed-world C1); the three chat levers (`CHATBENCH_0.8.0`: tier-1 331→333); the `../bedrock-meter` `$0`
65
+ rung (rank 0 below nova-micro, £0 metered e2e); the playtest; and the Stage-2/Stage-5 research notes.
66
+
67
+ **Next (post-release, in order):** (1) the judged re-judge of the stale tags (the deferred half of
68
+ the 0.8.2 evidence); (2) the chat-feel fast-follows from the confirmation playtest (recall
69
+ conjunction enforcement, the fuzzy-entity FALSE-EMPTY confident-wrong class, function-grain
70
+ coverage, module-grain overview); (3) the Track-1 trio against the measured red set (pron ×18,
71
+ temporal b1×5 + c1×9, disc-count re-measure first); (4) `edgesOfKind` memoization + endpoint
72
+ indices (perf at monorepo scale); (5) the seonix cutover ping + wh-scale feedback loop. Full list
73
+ with targets in `HANDOVER.md`.
45
74
 
46
75
  ## The umbrella product definition (item 1)
47
76
 
@@ -628,7 +657,7 @@ you toward precision" promise on the conversational surface.
628
657
  with new graded cells for the miss / empty-graph / concept-touch surfaces so these become
629
658
  regression-protected levers, not one-off polish.
630
659
 
631
- ## Phase 11 — The capability router & the agentic bench (0.8.0 LANDED)
660
+ ## Phase 11 — The capability router & the agentic bench (0.8.0 shipped · 0.8.1 deepened · 0.8.2 feel + rule-general C2)
632
661
 
633
662
  *(Operator-directed 2026-07-06; built the same day across five concurrent tracks.)* tmct as a **deterministic, no-LLM
634
663
  tool router** behind an Anthropic-compatible API — the workstream specified in
@@ -650,6 +679,14 @@ real deliverable. The five tracks below are all built; the two research-agent st
650
679
  remain designed-not-built by intent.
651
680
 
652
681
  ### Track 1 — chat-surface levers (next CHATBENCH; all three)
682
+
683
+ > **STATUS (0.8.2):** the surrounding feel surface landed — PLAN_CHAT_FEEL items **1–5, 7, 8**
684
+ > (recall hygiene, preamble frames, call-relation self-consistency, author lane, wall kindness,
685
+ > teach-lane widening, honest nudges) shipped and gate-verified deterministically. **The trio
686
+ > below is DEFERRED post-release with measured targets** (advisor tick-4): pronoun red set = 18
687
+ > g-b1-pron ids; temporal = g-b1-temp ×5 + g-c1-temp ×9; discourse-count re-measure first — it
688
+ > sampled 0/5 red and is likely already green. See HANDOVER follow-up #3.
689
+
653
690
  The three levers `CHATBENCH_0.7.1` measured + ranked — which **double as router prerequisites** (they
654
691
  gate the A2→B1→C1 rungs, per Phase B of the router plan):
655
692
  1. **Pronoun / focus binding** — the "it → Commit" mis-bind (`B1 pron 1.24`); biggest movable mass.
@@ -659,6 +696,12 @@ gate the A2→B1→C1 rungs, per Phase B of the router plan):
659
696
  Land all three (not just #1); they raise the chat floor *and* the router's floor at once.
660
697
 
661
698
  ### Track 2 — the router build (the within-horizon slice, in order)
699
+
700
+ > **STATUS (0.8.2):** the C1 composition gap closed — the **member-filter HTN method + per-member
701
+ > callees hop** flips the standing C1 red in both drivers (resolver floor A0–C1 all 100/100); the
702
+ > ladder grew 43→56 fixture-linted cases; the bench-import smell is inverted
703
+ > (`src/router/call-validator.mjs` + `set-algebra.mjs`).
704
+
662
705
  Buildable now with a frontier model as co-author (see PLAN §"solved vs unsolved"):
663
706
  - **Phase A — the shim.** An Anthropic Messages API endpoint (`/v1/messages`, `tool_use`/`tool_result`
664
707
  blocks). **Extended:** also present as a **`bedrock-meter`-compatible routing target** (see below).
@@ -697,6 +740,12 @@ critical build path:
697
740
  - **Stage 5 — goal-reasoner, closed-world C2** — BDI + Goal-Driven Autonomy: deduce-goals (long-chain
698
741
  deduction) → plan-each (C1) → threat-aware, *persistent* first-step arbitration.
699
742
 
743
+ > **STATUS (0.8.2):** both research stages are now BUILT and measured. Stage 2 landed in 0.8.1_002;
744
+ > Stage 5's 0.8.1 "one thin rule" caveat is retired — **C2 is rule-general**: two declared
745
+ > goal-rules (`coverage-invariant`, `cochange-risk-invariant`) selected by pure `applicableRules`
746
+ > deduction with honest refusals at both failure modes (0 applicable = open-world, >1 = ambiguous),
747
+ > zero request keywords. Goal driver: 100% plan / 98% result / 0% hallucination over 56 cases.
748
+
700
749
  ## Phase LATER — recognized, deferred, not now
701
750
 
702
751
  Features we have deliberately shaped seams for but will not build until the phases above have
@@ -60,7 +60,7 @@
60
60
  {"id":"conversational-greeting-good-evening","class":"conversational","register":"friendly","template":"Good evening. Ask me about this codebase, or /help."}
61
61
  {"id":"conversational-thanks","class":"conversational","register":"friendly","template":"Any time. Ask another, or /help for what I can do."}
62
62
  {"id":"conversational-farewell","class":"conversational","register":"friendly","template":"Bye — flushing the session log. Come back with a question any time."}
63
- {"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import walk.mjs\n what calls buildContextBundle\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
63
+ {"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import {example1}\n what calls {example2}\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
64
64
  {"id":"miss-no-previous-answer","class":"miss","register":"friendly","template":"No previous answer to expand yet — ask me a question first, then say \"why\" or \"say more\"."}
65
65
  {"id":"conversational-greeting-empty","class":"conversational","register":"friendly","template":"Hi. There's no code graph loaded here — for code structure (imports, calls, definitions) I need a `.tmct/graph.json`: point me at one with `--repo <path>`, or try the shipped example `npm run example:mini`. (tmct reads graphs; it doesn't index code itself.) For general vocabulary, `tmct init` seeds concepts — try \"what is a cache\". /help for commands."}
66
66
  {"id":"orientation-empty","class":"orientation","register":"friendly","template":"There's no code graph loaded here, so I can't answer structure questions (imports, calls, definitions) yet.\nFor those I need a `.tmct/graph.json` produced by a graph producer — point me at one with `--repo <path>`, or try the shipped example `npm run example:mini`. tmct reads graphs; it doesn't index code itself.\nFor general vocabulary, `tmct init` seeds concepts — try \"what is a cache\". /help for commands, /memory for what I remember."}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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
@@ -77,7 +77,12 @@ import { nlpAdapter } from "./ask-nlp.mjs";
77
77
  * classification, so they cannot drift in meaning). */
78
78
  function edgesOfKind(graph, kind) {
79
79
  const out = [];
80
- for (const g of graph.relations) if (relationKind(g) === kind) out.push(...g.edges);
80
+ // Plain-loop append, NOT out.push(...g.edges): argument spread overflows the call
81
+ // stack past ~100k edges on graph-scale relation groups (see codegraph.mjs twin).
82
+ for (const g of graph.relations) {
83
+ if (relationKind(g) !== kind) continue;
84
+ for (const e of g.edges) out.push(e);
85
+ }
81
86
  return out;
82
87
  }
83
88
 
@@ -89,6 +94,12 @@ function edgesOfKind(graph, kind) {
89
94
  // "which functions call X" should read off callsSymbol (fn->fn), not the module-coarse "calls".
90
95
  const SYMBOL_GRAIN_SIBLING = { calls: "callsSymbol", touches: "touchesSymbol" };
91
96
  const FINE_ENTITY_TYPES = new Set(["Function", "Method", "Class", "Attribute", "GlobalVariable"]);
97
+ // The fn/method FAMILY (0.8.2 WS1): callers of a symbol are recorded at whichever
98
+ // grain the extractor saw (a method Widget.render is class "Method"), but a person
99
+ // asking "which functions call X" means the callable family, not the storage class.
100
+ // Used ONLY as an empty-result fallback (see traverse's reverse symbol-grain path):
101
+ // an exact-class answer is never widened, so every non-empty answer is byte-stable.
102
+ const FINE_CLASS_SIBLING = { Function: "Method", Method: "Function" };
92
103
 
93
104
  // Query-side UNION families (2026-07-02 query families): a parsed kind that is not
94
105
  // itself a stored predicate but a curated union of stored kinds — "what uses X"
@@ -125,6 +136,24 @@ function verbFor(kind) {
125
136
  return REVERSE_MISS_VERB[kind] || kind;
126
137
  }
127
138
 
139
+ // Leading-relation-verb strip for the tests-kind honest empty (0.8.2 WS1): the
140
+ // keyword strategy can match the "tests" NOUN as the relation verb and leave the
141
+ // user's OWN verb at the head of the object term ("do any tests touch f.mjs" →
142
+ // object "touch f.mjs"), which the old ^cover-only strip missed ("No tests cover
143
+ // touch app/lib/f.mjs."). The closed list is read from ask-vocab.mjs's exported
144
+ // VERB_TO_KIND (derived from the RELATIONS verb table — the source of truth,
145
+ // including the `tests` kind's own verbs: cover/check/verify/exercise/…), longest
146
+ // phrase first so multi-word verbs strip whole; a bare optional s/ing/ed tail keeps
147
+ // the previously-stripped inflections ("covering") without enumerating them. Only
148
+ // ever applied to the tests-kind zero-hit template's object — never to resolution.
149
+ const LEADING_RELATION_VERB_RE = new RegExp(
150
+ `^(?:${Object.keys(VERB_TO_KIND)
151
+ .sort((a, b) => b.length - a.length)
152
+ .map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
153
+ .join("|")})(?:s|ing|ed)?\\s+`,
154
+ "i",
155
+ );
156
+
128
157
  // ---- the parsing strategies + normalization + fuzzy service formerly defined
129
158
  // here now live in src/interpret/ (items 8/10/13): interpret/normalize.mjs
130
159
  // (normalizeQuery, applyNegationFrames, STOPWORDS, splitWords), interpret/
@@ -1416,7 +1445,29 @@ export function traverse(graph, parsed, { contextId = null, prev = null } = {})
1416
1445
  const token = (i.attributes || []).find((a) => a.key === "token")?.value;
1417
1446
  return token && String(token).toLowerCase() === termLc;
1418
1447
  });
1419
- if (!match) return { matches: [], objMatch: null, candidates: [], traversal: `schema lookup for "${term}"`, ambiguous: false };
1448
+ if (!match) {
1449
+ // META FALLBACK TO REAL ENTITIES (0.8.2 WS1): "what is a Record" used to say
1450
+ // "'Record' isn't a term in this graph's own vocabulary" even when Record is a
1451
+ // code-graph Class individual. After the SchemaClass/SchemaPredicate miss, try
1452
+ // an exact case-insensitive UNIQUE label match against class === "Class"
1453
+ // individuals; a unique hit renders a describe-style one-liner (see render's
1454
+ // metaCodeClass branch). Anything less than a unique exact hit keeps the
1455
+ // honest vocabulary miss — never a guess.
1456
+ const classHits = (graph.individuals || []).filter((i) => i.class === "Class" && String(i.label).toLowerCase() === termLc);
1457
+ if (classHits.length === 1) {
1458
+ const hit = classHits[0];
1459
+ const mid = moduleIdOf(graph, hit);
1460
+ const modLabel = (mid && graph.byId.get(mid)?.label)
1461
+ || String((hit.attributes || []).find((a) => a.key === "site")?.value || "").split(":")[0]
1462
+ || null;
1463
+ return {
1464
+ matches: [hit], objMatch: hit, candidates: [], ambiguous: false,
1465
+ metaCodeClass: true, metaModuleLabel: modLabel,
1466
+ traversal: `schema lookup for "${term}" (miss), then unique Class individual by label`,
1467
+ };
1468
+ }
1469
+ return { matches: [], objMatch: null, candidates: [], traversal: `schema lookup for "${term}"`, ambiguous: false };
1470
+ }
1420
1471
  return {
1421
1472
  matches: [match], objMatch: match, candidates: [],
1422
1473
  traversal: `schema lookup for "${term}"`, ambiguous: false,
@@ -1536,9 +1587,22 @@ export function traverse(graph, parsed, { contextId = null, prev = null } = {})
1536
1587
  }
1537
1588
 
1538
1589
  if (shape === "forward") {
1539
- const edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.subject === objMatch.id);
1540
- const matches = edges.map((e) => graph.byId.get(e.object)).filter(Boolean);
1541
- return { matches, objMatch, candidates, traversal: `${kindsFor(kind).join("+")} edges where subject = ${objMatch.label}`, ambiguous, matchedVia };
1590
+ // FORWARD CALL UNION (0.8.2 WS1): a kind with a symbol-grain sibling scans the
1591
+ // UNION coarse+sibling when the resolved SUBJECT is itself a fine symbol —
1592
+ // "what does Widget.render call" lives on callsSymbol (fn->fn), which the
1593
+ // module-coarse scan alone can never reach (a coarse edge's subject is a
1594
+ // module, so a Function/Method subject rendered a false "no calls edges" while
1595
+ // the reverse direction answered). Module subjects never carry a sibling edge,
1596
+ // so their scan — and the traversal receipt — stays byte-identical. The receipt
1597
+ // names what was actually scanned ("calls+callsSymbol edges where subject = X").
1598
+ const fwdSibling = SYMBOL_GRAIN_SIBLING[kind];
1599
+ const subjIsFineSymbol = !!(fwdSibling && objMatch.class && FINE_ENTITY_TYPES.has(objMatch.class));
1600
+ const fwdKinds = subjIsFineSymbol ? [...new Set([...kindsFor(kind), fwdSibling])] : kindsFor(kind);
1601
+ const edges = fwdKinds.flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.subject === objMatch.id);
1602
+ const targets = edges.map((e) => graph.byId.get(e.object)).filter(Boolean);
1603
+ // dedupe only on the widened scan — the coarse-only path keeps its exact shape.
1604
+ const matches = subjIsFineSymbol ? uniqueById(targets) : targets;
1605
+ return { matches, objMatch, candidates, traversal: `${fwdKinds.join("+")} edges where subject = ${objMatch.label}`, ambiguous, matchedVia };
1542
1606
  }
1543
1607
 
1544
1608
  // reverse + transitive (PLAN_MECHANICAL_CHAT.md P1): the gate above guarantees kind is
@@ -1577,8 +1641,23 @@ export function traverse(graph, parsed, { contextId = null, prev = null } = {})
1577
1641
  if (symbolKind && (FINE_ENTITY_TYPES.has(entityType) || objIsFineSymbol)) {
1578
1642
  const edges = edgesOfKind(graph, symbolKind).filter((e) => e.object === objMatch.id);
1579
1643
  const subjects = uniqueById(edges.map((e) => graph.byId.get(e.subject)).filter(Boolean));
1580
- const matches = (!entityType || entityType === "Change") ? subjects : subjects.filter((i) => i.class === entityType);
1581
- return { matches, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}`, ambiguous, matchedVia };
1644
+ let matches = (!entityType || entityType === "Change") ? subjects : subjects.filter((i) => i.class === entityType);
1645
+ let widenNote = "";
1646
+ // FINE-GRAIN FAMILY FALLBACK (0.8.2 WS1): when the exact-class filter comes back
1647
+ // EMPTY and the asked grain is Function/Method, retry with the family sibling —
1648
+ // "which functions call fnAlpha" must not hide the recorded caller Widget.render
1649
+ // just because the extractor stored it as class Method. Fallback-only by
1650
+ // construction (the exact filter must be empty first), so every currently
1651
+ // non-empty answer is byte-identical; the widening is said in the traversal.
1652
+ const siblingClass = FINE_CLASS_SIBLING[entityType];
1653
+ if (!matches.length && siblingClass) {
1654
+ const widened = subjects.filter((i) => i.class === siblingClass);
1655
+ if (widened.length) {
1656
+ matches = widened;
1657
+ widenNote = `, widened to ${siblingClass} subjects (no ${entityType} recorded)`;
1658
+ }
1659
+ }
1660
+ return { matches, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}${widenNote}`, ambiguous, matchedVia };
1582
1661
  }
1583
1662
 
1584
1663
  // General case: some predicates are already fine-grained (inherits: Class->Class, contains:
@@ -1646,6 +1725,17 @@ function symbolLabelOf(ind) {
1646
1725
  return ["Function", "Method"].includes(ind.class) ? `function ${label}()` : label;
1647
1726
  }
1648
1727
 
1728
+ /** A FRIENDLY commit reference for a "who touched X" list — the raw sha alone reads as
1729
+ * noise, so when the Commit individual carries an author (mgx:commitAuthor → key
1730
+ * "author") name them beside it. The label is already the graph's short ref (the
1731
+ * builder stores sha.slice(0,12)), so it is used verbatim. Degrades gracefully: a
1732
+ * commit with no recorded author renders the sha alone, exactly as before. */
1733
+ function commitRefOf(ind) {
1734
+ const sha = String(ind.label || ind.id || "");
1735
+ const author = (ind.attributes || []).find((a) => a.key === "author")?.value;
1736
+ return author ? `${sha} (${author})` : sha;
1737
+ }
1738
+
1649
1739
  function listJoin(syms) {
1650
1740
  return syms.length > 1 ? `${syms.slice(0, -1).join(", ")} and ${syms[syms.length - 1]}` : syms[0];
1651
1741
  }
@@ -1703,6 +1793,17 @@ function renderCore(parsed, result) {
1703
1793
  miss: true, ambiguous: false,
1704
1794
  };
1705
1795
  }
1796
+ // meta fallback hit (0.8.2 WS1, see traverse's meta branch): the term is not
1797
+ // schema vocabulary but IS a unique code-graph Class — a describe-style
1798
+ // one-liner pointing at the real entity, instead of the false vocabulary miss.
1799
+ if (result.metaCodeClass) {
1800
+ const label = result.objMatch.label;
1801
+ const definedIn = result.metaModuleLabel ? `, defined in ${result.metaModuleLabel}` : "";
1802
+ return {
1803
+ content: `${label} is a class in this codebase${definedIn} — try "describe ${label}" or "which classes inherit from ${label}".`,
1804
+ miss: false, ambiguous: false, matches: result.matches,
1805
+ };
1806
+ }
1706
1807
  const doc = (result.objMatch.attributes || []).find((a) => a.key === "doc")?.value || "";
1707
1808
  const kindWord = result.objMatch.class === "SchemaClass" ? "a class in the graph's schema" : "a predicate (relation) in the graph's schema";
1708
1809
  return { content: `${result.objMatch.label} is ${kindWord}: ${doc}`, miss: false, ambiguous: false, matches: result.matches };
@@ -1713,7 +1814,7 @@ function renderCore(parsed, result) {
1713
1814
  if (result.mentionsShape) {
1714
1815
  if (!result.matches.length) {
1715
1816
  return {
1716
- content: `"${parsed.object}" is not mentioned in any indexed identifier or doc-comment prose. (traversal: ${result.traversal})`,
1817
+ content: `"${parsed.object}" is not mentioned in any indexed identifier or doc-comment prose.`,
1717
1818
  miss: true, ambiguous: false,
1718
1819
  };
1719
1820
  }
@@ -1770,7 +1871,7 @@ function renderCore(parsed, result) {
1770
1871
  if (result.whenShape) {
1771
1872
  const subject = result.objMatch.label;
1772
1873
  if (!result.matches.length) {
1773
- return { content: `no recorded commit touches ${subject} in this index. (traversal: ${result.traversal})`, miss: true, ambiguous: false };
1874
+ return { content: `no recorded commit touches ${subject} in this index.`, miss: true, ambiguous: false };
1774
1875
  }
1775
1876
  const newest = result.matches[0];
1776
1877
  const date = (newest.attributes || []).find((a) => a.key === "date")?.value || "";
@@ -1800,7 +1901,7 @@ function renderCore(parsed, result) {
1800
1901
  const cite = `commit ${result.objMatch.label}`;
1801
1902
  if (!result.matches.length) {
1802
1903
  return {
1803
- content: `${cite} touched nothing recorded in the index. (traversal: ${result.traversal})`,
1904
+ content: `${cite} touched nothing recorded in the index.`,
1804
1905
  miss: true, ambiguous: false,
1805
1906
  };
1806
1907
  }
@@ -1818,8 +1919,11 @@ function renderCore(parsed, result) {
1818
1919
  if (!result.objMatch || !result.subjMatch) {
1819
1920
  return { content: `couldn't resolve one of the terms in this question.`, miss: true, ambiguous: false };
1820
1921
  }
1922
+ // the yes render is plain words — the traversal string IS "<kind> edge from
1923
+ // <A> to <B>", so it reads as the sentence itself, not a parenthetical receipt
1924
+ // (the receipt still rides on the result's traversal field for why/verbose).
1821
1925
  return {
1822
- content: result.answer ? `Yes. (${result.traversal})` : `No — no ${parsed.kind} edge found from ${result.subjMatch.label} to ${result.objMatch.label}.`,
1926
+ content: result.answer ? `Yes ${result.traversal}.` : `No — no ${parsed.kind} edge found from ${result.subjMatch.label} to ${result.objMatch.label}.`,
1823
1927
  miss: !result.answer, ambiguous: false,
1824
1928
  };
1825
1929
  }
@@ -1830,7 +1934,24 @@ function renderCore(parsed, result) {
1830
1934
  // subject-first phrasing rather than reusing reverse's "found ... that OBJECT" template.
1831
1935
  if (parsed.shape === "forward") {
1832
1936
  return {
1833
- content: `${result.objMatch.label} has no ${parsed.kind} edges in the index. (traversal: ${result.traversal || "no traversal resolved"})`,
1937
+ content: `${result.objMatch.label} has no ${parsed.kind} edges in the index.`,
1938
+ miss: true, ambiguous: false,
1939
+ };
1940
+ }
1941
+ // "what tests cover X" / "what tests X" — the tests themselves are the search
1942
+ // target (no explicit entity keyword → entityType null), and "tests" reads as a
1943
+ // verb phrase, so the generic "No <modules> found whose module directly tests <obj>"
1944
+ // template garbles: it mislabels the searched kind as "modules" and lets the user's
1945
+ // leaked verb ride into the object ("…tests cover touch X"). Any leading relation
1946
+ // verb (cover/touch/check/verify/… — LEADING_RELATION_VERB_RE, built from the
1947
+ // ask-vocab verb table) is stripped, so the honest empty reads as the natural
1948
+ // "No tests cover X." The frozen entity-keyword form ("which modules test X",
1949
+ // entityType="Module") keeps its pinned wording below.
1950
+ if (parsed.kind === "tests" && !parsed.entityType) {
1951
+ const stripped = String(parsed.object || "").replace(LEADING_RELATION_VERB_RE, "").trim();
1952
+ const obj = stripped || String(parsed.object || "").trim();
1953
+ return {
1954
+ content: `No tests cover ${obj}.`,
1834
1955
  miss: true, ambiguous: false,
1835
1956
  };
1836
1957
  }
@@ -1840,7 +1961,7 @@ function renderCore(parsed, result) {
1840
1961
  // append-only/sacred mid-arc, so the honest-miss phrasing stays as-is.
1841
1962
  const entityWord = nounFor(parsed.entityType || "Module", 2);
1842
1963
  return {
1843
- content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}. (traversal: ${result.traversal || "no traversal resolved"})`,
1964
+ content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}.`,
1844
1965
  miss: true, ambiguous: false,
1845
1966
  };
1846
1967
  }
@@ -1853,7 +1974,10 @@ function renderCore(parsed, result) {
1853
1974
  // Commit list ("which commits touched X") has no containing module to group by, so
1854
1975
  // anything that is not a fine entity takes the flat join.
1855
1976
  if (parsed.shape === "forward" || parsed.entityType === "Module" || result.matches.every((m) => !FINE_ENTITY_TYPES.has(m.class))) {
1856
- const shown = result.matches.slice(0, OVERFLOW_CAP).map((m) => m.label);
1977
+ // A reverse "who touched X" resolves to Commit individuals — render friendly refs
1978
+ // (short sha + author) instead of the raw stored sha; every other flat list (module
1979
+ // labels, etc.) keeps its own label verbatim.
1980
+ const shown = result.matches.slice(0, OVERFLOW_CAP).map((m) => m.class === "Commit" ? commitRefOf(m) : m.label);
1857
1981
  const extra = result.matches.length > OVERFLOW_CAP ? `, …and ${result.matches.length - OVERFLOW_CAP} more` : "";
1858
1982
  return { content: shown.join(" and ") + extra + ".", miss: false, ambiguous: false, matches: result.matches };
1859
1983
  }