@polycode-projects/the-mechanical-code-talker 0.8.1 → 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,11 +12,29 @@ 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.1Phase 11 deepened (0.8.0 shipped + published; 0.8.1 push held for the operator).**
18
- `npm test` green (**916**). 0.8.1 hardened the router with real *reasoning* measurement and closed the
19
- frontier stages, all under the same demonstrated-vs-designed honesty discipline:
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:
20
38
 
21
39
  - **AGENTBENCH now grades the executed composed RESULT, not just the call-plan** (retires the headline
22
40
  0.8.0 caveat). Under result-grading the resolver is **97% plan / 91% result / 0% hallucination**; the
@@ -46,9 +64,13 @@ registry + resolver/guardrail/planner (`AGENTBENCH_0.8.0_001`: 96% *plan* comple
46
64
  closed-world C1); the three chat levers (`CHATBENCH_0.8.0`: tier-1 331→333); the `../bedrock-meter` `$0`
47
65
  rung (rank 0 below nova-micro, £0 metered e2e); the playtest; and the Stage-2/Stage-5 research notes.
48
66
 
49
- **Next:** grow the thin C2/coverage sampling to a second goal-rule; author→commit querying (a dead-end
50
- the friendly-commit-ref quick win opened); AGENTBENCH ladder depth; the tier-4/tier-5 Phase LATER long
51
- tail. See Phase 11 below and `HANDOVER.md` for the full open-follow-up list.
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`.
52
74
 
53
75
  ## The umbrella product definition (item 1)
54
76
 
@@ -635,7 +657,7 @@ you toward precision" promise on the conversational surface.
635
657
  with new graded cells for the miss / empty-graph / concept-touch surfaces so these become
636
658
  regression-protected levers, not one-off polish.
637
659
 
638
- ## Phase 11 — The capability router & the agentic bench (0.8.0 shipped · 0.8.1 deepened)
660
+ ## Phase 11 — The capability router & the agentic bench (0.8.0 shipped · 0.8.1 deepened · 0.8.2 feel + rule-general C2)
639
661
 
640
662
  *(Operator-directed 2026-07-06; built the same day across five concurrent tracks.)* tmct as a **deterministic, no-LLM
641
663
  tool router** behind an Anthropic-compatible API — the workstream specified in
@@ -657,6 +679,14 @@ real deliverable. The five tracks below are all built; the two research-agent st
657
679
  remain designed-not-built by intent.
658
680
 
659
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
+
660
690
  The three levers `CHATBENCH_0.7.1` measured + ranked — which **double as router prerequisites** (they
661
691
  gate the A2→B1→C1 rungs, per Phase B of the router plan):
662
692
  1. **Pronoun / focus binding** — the "it → Commit" mis-bind (`B1 pron 1.24`); biggest movable mass.
@@ -666,6 +696,12 @@ gate the A2→B1→C1 rungs, per Phase B of the router plan):
666
696
  Land all three (not just #1); they raise the chat floor *and* the router's floor at once.
667
697
 
668
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
+
669
705
  Buildable now with a frontier model as co-author (see PLAN §"solved vs unsolved"):
670
706
  - **Phase A — the shim.** An Anthropic Messages API endpoint (`/v1/messages`, `tool_use`/`tool_result`
671
707
  blocks). **Extended:** also present as a **`bedrock-meter`-compatible routing target** (see below).
@@ -704,6 +740,12 @@ critical build path:
704
740
  - **Stage 5 — goal-reasoner, closed-world C2** — BDI + Goal-Driven Autonomy: deduce-goals (long-chain
705
741
  deduction) → plan-each (C1) → threat-aware, *persistent* first-step arbitration.
706
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
+
707
749
  ## Phase LATER — recognized, deferred, not now
708
750
 
709
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.1",
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:
@@ -1714,6 +1793,17 @@ function renderCore(parsed, result) {
1714
1793
  miss: true, ambiguous: false,
1715
1794
  };
1716
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
+ }
1717
1807
  const doc = (result.objMatch.attributes || []).find((a) => a.key === "doc")?.value || "";
1718
1808
  const kindWord = result.objMatch.class === "SchemaClass" ? "a class in the graph's schema" : "a predicate (relation) in the graph's schema";
1719
1809
  return { content: `${result.objMatch.label} is ${kindWord}: ${doc}`, miss: false, ambiguous: false, matches: result.matches };
@@ -1724,7 +1814,7 @@ function renderCore(parsed, result) {
1724
1814
  if (result.mentionsShape) {
1725
1815
  if (!result.matches.length) {
1726
1816
  return {
1727
- 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.`,
1728
1818
  miss: true, ambiguous: false,
1729
1819
  };
1730
1820
  }
@@ -1781,7 +1871,7 @@ function renderCore(parsed, result) {
1781
1871
  if (result.whenShape) {
1782
1872
  const subject = result.objMatch.label;
1783
1873
  if (!result.matches.length) {
1784
- 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 };
1785
1875
  }
1786
1876
  const newest = result.matches[0];
1787
1877
  const date = (newest.attributes || []).find((a) => a.key === "date")?.value || "";
@@ -1811,7 +1901,7 @@ function renderCore(parsed, result) {
1811
1901
  const cite = `commit ${result.objMatch.label}`;
1812
1902
  if (!result.matches.length) {
1813
1903
  return {
1814
- content: `${cite} touched nothing recorded in the index. (traversal: ${result.traversal})`,
1904
+ content: `${cite} touched nothing recorded in the index.`,
1815
1905
  miss: true, ambiguous: false,
1816
1906
  };
1817
1907
  }
@@ -1829,8 +1919,11 @@ function renderCore(parsed, result) {
1829
1919
  if (!result.objMatch || !result.subjMatch) {
1830
1920
  return { content: `couldn't resolve one of the terms in this question.`, miss: true, ambiguous: false };
1831
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).
1832
1925
  return {
1833
- 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}.`,
1834
1927
  miss: !result.answer, ambiguous: false,
1835
1928
  };
1836
1929
  }
@@ -1841,21 +1934,24 @@ function renderCore(parsed, result) {
1841
1934
  // subject-first phrasing rather than reusing reverse's "found ... that OBJECT" template.
1842
1935
  if (parsed.shape === "forward") {
1843
1936
  return {
1844
- 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.`,
1845
1938
  miss: true, ambiguous: false,
1846
1939
  };
1847
1940
  }
1848
1941
  // "what tests cover X" / "what tests X" — the tests themselves are the search
1849
1942
  // target (no explicit entity keyword → entityType null), and "tests" reads as a
1850
1943
  // verb phrase, so the generic "No <modules> found whose module directly tests <obj>"
1851
- // template garbles: it mislabels the searched kind as "modules" and lets the leaked
1852
- // "cover " verb ride into the object ("…directly tests cover X"). Render the honest
1853
- // empty as the natural "No tests cover X." The frozen entity-keyword form ("which
1854
- // modules test X", entityType="Module") keeps its pinned wording below.
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.
1855
1950
  if (parsed.kind === "tests" && !parsed.entityType) {
1856
- const obj = String(parsed.object || "").replace(/^cover(?:s|ing)?\s+/i, "").trim();
1951
+ const stripped = String(parsed.object || "").replace(LEADING_RELATION_VERB_RE, "").trim();
1952
+ const obj = stripped || String(parsed.object || "").trim();
1857
1953
  return {
1858
- content: `No tests cover ${obj}. (traversal: ${result.traversal || "no traversal resolved"})`,
1954
+ content: `No tests cover ${obj}.`,
1859
1955
  miss: true, ambiguous: false,
1860
1956
  };
1861
1957
  }
@@ -1865,7 +1961,7 @@ function renderCore(parsed, result) {
1865
1961
  // append-only/sacred mid-arc, so the honest-miss phrasing stays as-is.
1866
1962
  const entityWord = nounFor(parsed.entityType || "Module", 2);
1867
1963
  return {
1868
- 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}.`,
1869
1965
  miss: true, ambiguous: false,
1870
1966
  };
1871
1967
  }