@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 +80 -31
- package/data/templates/responses.jsonl +1 -1
- package/package.json +1 -1
- package/src/ask.mjs +138 -14
- package/src/chat.mjs +369 -20
- package/src/codegraph.mjs +109 -1
- package/src/interpret/merge.mjs +16 -1
- package/src/interpret/normalize.mjs +144 -2
- package/src/interpret/pipeline.mjs +18 -3
- package/src/interpret/strategies/ace.mjs +49 -0
- package/src/memory/core.mjs +8 -1
- package/src/memory/fold.mjs +0 -0
- package/src/memory/trust.mjs +8 -4
- package/src/router/call-validator.mjs +45 -0
- package/src/router/goal-reasoner.mjs +364 -0
- package/src/router/guardrail.mjs +1 -1
- package/src/router/planner.mjs +22 -1
- package/src/router/resolver.mjs +49 -11
- package/src/router/set-algebra.mjs +31 -0
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-
|
|
16
|
-
|
|
17
|
-
**Built: v0.8.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- **
|
|
27
|
-
(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
**
|
|
43
|
-
|
|
44
|
-
`
|
|
15
|
+
## Where we are now (2026-07-07)
|
|
16
|
+
|
|
17
|
+
**Built: v0.8.2 — the 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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
-
|
|
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
|
}
|