@polycode-projects/the-mechanical-code-talker 1.8.10 → 1.8.12
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 +2 -2
- package/ROADMAP.md +9 -6
- package/package.json +1 -1
- package/src/ask.mjs +62 -15
- package/src/chat.mjs +171 -5
package/README.md
CHANGED
|
@@ -360,8 +360,8 @@ on the chat's hot path.
|
|
|
360
360
|
## What tmct deliberately is NOT
|
|
361
361
|
|
|
362
362
|
- **It is not an indexer.** tmct keeps no codebase index of its own. It
|
|
363
|
-
consumes a graph via a provider seam (`fetchEntities` and friends)
|
|
364
|
-
|
|
363
|
+
consumes a graph via a provider seam (`fetchEntities` and friends) — building
|
|
364
|
+
that graph is a different tool's job. tmct's job is the *conversation*.
|
|
365
365
|
- **It is not a reasoning model.** Where it "reasons", it does so by
|
|
366
366
|
*calculation* surfaced as prose ("there are a lot of tests for a codebase of
|
|
367
367
|
that size"). It is deterministic, explainable, and cheap. Even its forward-chaining
|
package/ROADMAP.md
CHANGED
|
@@ -18,8 +18,9 @@ Declared, forward-looking goals — not yet achieved, stated here so they steer
|
|
|
18
18
|
getting silently traded away by inherited caution:
|
|
19
19
|
|
|
20
20
|
- **Reach for Llama-3-level natural language fluency.** Not by putting an LLM in the product path
|
|
21
|
-
(
|
|
22
|
-
variety, so an answer shape has many valid phrasings instead of one
|
|
21
|
+
(tmct will never do that — see "What tmct will never do"). Instead, by growing rich
|
|
22
|
+
template/surface-realization variety, so an answer shape has many valid phrasings instead of one
|
|
23
|
+
fixed slot-fill.
|
|
23
24
|
- **Resolve ambiguity breadth-first, always.** Every genuinely valid reading gets its own real answer
|
|
24
25
|
restated in full, never a bare "could mean X or Y — try rephrasing" punt, bounded only by existing
|
|
25
26
|
clipping/pagination limits. Landed for both ambiguity shapes tmct has: parse-level ties
|
|
@@ -219,11 +220,13 @@ original question, citing what was just learned. Strictly opt-in, offline defaul
|
|
|
219
220
|
Prerequisites: the provenance-trust policy must extend to `via:"learned:web"`, never silently
|
|
220
221
|
blending web-sourced facts with graph/operator facts.
|
|
221
222
|
|
|
222
|
-
##
|
|
223
|
+
## What tmct will never do
|
|
223
224
|
|
|
224
|
-
-
|
|
225
|
-
|
|
226
|
-
-
|
|
225
|
+
- Run on AWS or maintain a benchmark rig — tmct is a published npm library + CLI with a static
|
|
226
|
+
GitLab Pages home page, nothing more.
|
|
227
|
+
- Publish automatically — a version release always requires a deliberate version-bump commit.
|
|
228
|
+
- Run an MCP server or put an LLM in the product path — a core identity decision, decided once,
|
|
229
|
+
not revisited.
|
|
227
230
|
|
|
228
231
|
## Design docs
|
|
229
232
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.12",
|
|
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
|
@@ -1571,9 +1571,23 @@ function membershipOwnSet(graph, id, entityType) {
|
|
|
1571
1571
|
* path prefix with no exact node of its own — see directoryScopeModules's own
|
|
1572
1572
|
* doc) or a single container individual, exactly like the "membership" case's own
|
|
1573
1573
|
* pre-item-6 resolution — extracted unchanged so evalSet's plain path and the
|
|
1574
|
-
* composite/disclosure path (evalMembershipComposite) can never drift.
|
|
1575
|
-
|
|
1576
|
-
|
|
1574
|
+
* composite/disclosure path (evalMembershipComposite) can never drift.
|
|
1575
|
+
*
|
|
1576
|
+
* `contextId` (HANDOVER.md 2026-07-12 finding, fast-loop round 4): this used to
|
|
1577
|
+
* call bare `resolveObject(graph, term)` with no context-pronoun notion at all —
|
|
1578
|
+
* "methods of that"/"attributes of it" reached resolveObjectCore's ordinary
|
|
1579
|
+
* mechanical tiers with the raw pronoun string, an honest miss at best and, at
|
|
1580
|
+
* worst, a false-positive substring hit (a 2-4 letter pronoun is a near-certain
|
|
1581
|
+
* accidental substring of SOME real label — the exact
|
|
1582
|
+
* STACCATO_LEAKED_CONNECTIVES trap chat.mjs documents for "it"/"and"). Routed
|
|
1583
|
+
* through resolveTermOrContext instead — the SAME contextId-aware resolution
|
|
1584
|
+
* evalQualCheck and traverse()'s reverse/forward shapes already use for
|
|
1585
|
+
* subject-/object-position pronouns — so a pronoun binds to the standing focus
|
|
1586
|
+
* and a non-pronoun term resolves byte-identically to before (resolveTermOrContext
|
|
1587
|
+
* falls through to the same bare `resolveObject` call for anything that isn't a
|
|
1588
|
+
* CONTEXT_PRONOUNS member). */
|
|
1589
|
+
function resolveMembershipOwner(graph, term, contextId = null) {
|
|
1590
|
+
const r = resolveTermOrContext(graph, term, contextId);
|
|
1577
1591
|
if (!(r.match && r.tier === 1)) {
|
|
1578
1592
|
const dirMods = directoryScopeModules(graph, term);
|
|
1579
1593
|
if (dirMods.length) return { kind: "dir", mods: dirMods };
|
|
@@ -1854,7 +1868,7 @@ function evalSet(graph, ast, opts) {
|
|
|
1854
1868
|
// node match (tier 1 — a real file/symbol named that) still wins outright
|
|
1855
1869
|
// (unchanged single-container-node behavior, e.g. "methods in widget.mjs");
|
|
1856
1870
|
// only when there is no exact match do we try directory-prefix scope first.
|
|
1857
|
-
const owner = resolveMembershipOwner(graph, ast.term);
|
|
1871
|
+
const owner = resolveMembershipOwner(graph, ast.term, opts && opts.contextId);
|
|
1858
1872
|
if (owner.kind === "dir") {
|
|
1859
1873
|
if (!ast.entityType || ast.entityType === "Module") return owner.mods;
|
|
1860
1874
|
const ids = new Set(owner.mods.map((m) => m.id));
|
|
@@ -1977,8 +1991,14 @@ function evalAnaphora(graph, ast, opts) {
|
|
|
1977
1991
|
// commit-history kinds are excluded so "connections" reads as the code-structure
|
|
1978
1992
|
// degree a developer means, not every recorded touch.
|
|
1979
1993
|
const DEGREE_KINDS = ["imports", "calls", "callsSymbol", "inherits", "contains", "tests"];
|
|
1980
|
-
/** Degree of an individual under a superlative metric ({kind, dir, sibling?, filter?}).
|
|
1981
|
-
|
|
1994
|
+
/** Degree of an individual under a superlative metric ({kind, dir, sibling?, filter?}).
|
|
1995
|
+
* Exported (2026-07-12, HANDOVER "bare 'how many X' fails for edge-nominalized
|
|
1996
|
+
* nouns" fix) so chat.mjs's answerEdgeCount can compute the SAME per-entity
|
|
1997
|
+
* degree for a single named entity ("how many callers does X have") that this
|
|
1998
|
+
* file's own evalSuperlative already uses to rank every entity of a class
|
|
1999
|
+
* ("which module has the most callers") — one metric definition
|
|
2000
|
+
* (EDGE_NOUN_TO_METRIC), one degree computation, two call sites. */
|
|
2001
|
+
export function degreeMetric(graph, ind, metric) {
|
|
1982
2002
|
const kinds = metric.kind === "*" ? DEGREE_KINDS : [metric.kind, ...(metric.sibling ? [metric.sibling] : [])];
|
|
1983
2003
|
let n = 0;
|
|
1984
2004
|
for (const k of kinds) for (const e of edgesOfKind(graph, k)) {
|
|
@@ -2088,7 +2108,7 @@ function evalMembershipComposite(graph, ast, opts) {
|
|
|
2088
2108
|
const filterFn = qualNode
|
|
2089
2109
|
? (ind) => qualNode.filters.every((f) => qualHolds(graph, ind, QUALIFIERS[f]))
|
|
2090
2110
|
: null;
|
|
2091
|
-
const owner = resolveMembershipOwner(graph, memNode.term);
|
|
2111
|
+
const owner = resolveMembershipOwner(graph, memNode.term, opts && opts.contextId);
|
|
2092
2112
|
if (owner.kind === "dir") {
|
|
2093
2113
|
let objs;
|
|
2094
2114
|
if (!entityType || entityType === "Module") objs = owner.mods;
|
|
@@ -3426,7 +3446,24 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
3426
3446
|
widenNote = `, widened to ${siblingClass} subjects (no ${entityType} recorded)`;
|
|
3427
3447
|
}
|
|
3428
3448
|
}
|
|
3429
|
-
|
|
3449
|
+
// "touches" up-refine composability (HANDOVER 2026-07-12, Class-to-module
|
|
3450
|
+
// up-refinement): an EMPTY touchesSymbol lookup on a resolved CLASS is not
|
|
3451
|
+
// decisive the way it is for a Function/Method — a Class reads naturally as
|
|
3452
|
+
// "the file/unit that holds it", so "who touched <Class>" with no recorded
|
|
3453
|
+
// symbol-precise touch should still answer from the class's containing
|
|
3454
|
+
// module's real touches, rather than a confident-looking-but-possibly-wrong
|
|
3455
|
+
// "nothing touched it". Fall through to the grain-aware up-refine below
|
|
3456
|
+
// ONLY for that Class case. Deliberately NOT widened to every
|
|
3457
|
+
// FINE_ENTITY_TYPES member: "how many commits touched fnAlpha" (a Function)
|
|
3458
|
+
// is pinned elsewhere (ask-combo.test.mjs's grain-aware COUNT lever) to stay
|
|
3459
|
+
// an honest 0 rather than a module-grain false hit — symbol-level counting
|
|
3460
|
+
// precision for functions/methods is a deliberate, separate guarantee this
|
|
3461
|
+
// change must not erode. `calls` is untouched either way: an empty
|
|
3462
|
+
// callsSymbol result stays decisive (call parsing isn't a best-effort
|
|
3463
|
+
// heuristic the way commit-diff symbol attribution is).
|
|
3464
|
+
if (matches.length || !(kind === "touches" && objMatch.class === "Class")) {
|
|
3465
|
+
return { matches, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}${widenNote}`, ambiguous, matchedVia };
|
|
3466
|
+
}
|
|
3430
3467
|
}
|
|
3431
3468
|
|
|
3432
3469
|
// §grain-aware object resolution (Bug C+D, HANDOVER follow-up #2, checked BEFORE
|
|
@@ -3457,13 +3494,23 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
3457
3494
|
gCandidates = retry.candidates;
|
|
3458
3495
|
gAmbiguous = retry.ambiguous;
|
|
3459
3496
|
gMatchedVia = retry.matchedVia;
|
|
3460
|
-
} else if (
|
|
3461
|
-
// (2)
|
|
3462
|
-
//
|
|
3463
|
-
//
|
|
3464
|
-
//
|
|
3465
|
-
//
|
|
3466
|
-
//
|
|
3497
|
+
} else if (wantClass === "Module") {
|
|
3498
|
+
// (2) up-refine to the containing module — driven by kindObjectClass
|
|
3499
|
+
// itself (any kind whose real object-class is ALWAYS Module: tests,
|
|
3500
|
+
// cochange, imports, touches, …), not a hardcoded kind name list, so a
|
|
3501
|
+
// kind newly recorded as Module->Module in the graph gets this for free.
|
|
3502
|
+
// No same-grain alternative exists here (the retry above genuinely found
|
|
3503
|
+
// nothing), but the resolved fine-grain entity (a Function/Class, say)
|
|
3504
|
+
// DOES live in a module, and that module is the real, honest subject of
|
|
3505
|
+
// the question ("does createTask have tests" — Bug D; "who touched Bar",
|
|
3506
|
+
// "what modules import Bar" — the same up-refine extended past
|
|
3507
|
+
// tests/cochange, HANDOVER 2026-07-12). `calls` computes to Module here
|
|
3508
|
+
// too, but never actually reaches this branch with a wrong-grain object:
|
|
3509
|
+
// the symbolKind branch above already intercepts every Class/Function/…
|
|
3510
|
+
// object for `calls` unconditionally (its empty-result IS decisive, see
|
|
3511
|
+
// that branch's own comment), so this is inert-but-correct for it. Up-
|
|
3512
|
+
// refine via the same moduleIdOf qualHolds's "tested" case already uses
|
|
3513
|
+
// (see its divergence comment above).
|
|
3467
3514
|
const mid = moduleIdOf(graph, gObjMatch);
|
|
3468
3515
|
const mod = mid && graph.byId.get(mid);
|
|
3469
3516
|
if (mod) {
|
package/src/chat.mjs
CHANGED
|
@@ -57,7 +57,7 @@ import { rankByBiasThenTrust } from "./memory/bias.mjs";
|
|
|
57
57
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
58
58
|
import {
|
|
59
59
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
60
|
-
stripTrailingScopeFiller, stripTrailingDiscourseTag,
|
|
60
|
+
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS,
|
|
61
61
|
} from "./ask-vocab.mjs";
|
|
62
62
|
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, normalizeQuery, escapeRegex, kindNounAnaphoraHint } from "./interpret/normalize.mjs";
|
|
63
63
|
import { fuzzyMatchInSet, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
@@ -564,6 +564,113 @@ export function answerCount(graph, query) {
|
|
|
564
564
|
return `${n} ${classNoun(cls, n)}.`;
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
/** singular+plural display forms for the edge-nominalized nouns answerEdgeCount
|
|
568
|
+
* (below) actually answers — a small subset of EDGE_NOUN_TO_METRIC's keys, the
|
|
569
|
+
* ones that read as a real countable noun ("3 callers.") rather than a
|
|
570
|
+
* participle only natural in a superlative ("most USED", not "how many used").
|
|
571
|
+
* A key with no entry here echoes the user's own word unchanged (safe default,
|
|
572
|
+
* same fallback CLASS_LABELS/classNoun use above). */
|
|
573
|
+
const EDGE_NOUN_LABELS = {
|
|
574
|
+
test: ["test", "tests"], tests: ["test", "tests"],
|
|
575
|
+
importers: ["importer", "importers"], dependents: ["dependent", "dependents"],
|
|
576
|
+
callers: ["caller", "callers"], callees: ["callee", "callees"],
|
|
577
|
+
calls: ["call", "calls"], imports: ["import", "imports"],
|
|
578
|
+
dependencies: ["dependency", "dependencies"], members: ["member", "members"],
|
|
579
|
+
subclasses: ["subclass", "subclasses"], connections: ["connection", "connections"],
|
|
580
|
+
edges: ["edge", "edges"],
|
|
581
|
+
};
|
|
582
|
+
const edgeCountNoun = (noun, n) => {
|
|
583
|
+
const [s, p] = EDGE_NOUN_LABELS[noun] || [noun, noun];
|
|
584
|
+
return n === 1 ? s : p;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/** Pull the named entity out of a per-entity edge-count tail — the text after
|
|
588
|
+
* "how many <edge-noun>" — recognising exactly the two closed shapes such a
|
|
589
|
+
* tail actually takes:
|
|
590
|
+
* - "<verb> <entity>" ("cover src/x.mjs", "import Widget") — verb drawn
|
|
591
|
+
* from ask-vocab.mjs's RELATIONS[metric.kind].verbs (the SAME verb list
|
|
592
|
+
* the relation clause grammar itself reads), longest-first so a
|
|
593
|
+
* multi-word verb ("depends on") matches whole rather than a short
|
|
594
|
+
* prefix stealing part of the entity name.
|
|
595
|
+
* - "does/do/did <entity> have/has/had/got" ("does X have") — safe to
|
|
596
|
+
* treat as unambiguous HERE even though answerCount's own
|
|
597
|
+
* AMBIGUOUS_HAVE_VERBS guard deliberately excludes "have" elsewhere:
|
|
598
|
+
* that guard exists because "have" maps to either defines/contains
|
|
599
|
+
* depending on the SUBJECT's class, but an edge-nominalized noun's
|
|
600
|
+
* metric.dir is fixed by the NOUN itself ("importers" is always dir
|
|
601
|
+
* "in"), so there is no analogous ambiguity to worry about here.
|
|
602
|
+
* Returns the trimmed entity term, or null (no recognizable shape — an
|
|
603
|
+
* honest decline, not a guess) — the caller then leaves the existing "I
|
|
604
|
+
* can't count" message from answerCount standing. */
|
|
605
|
+
function extractEdgeCountEntity(tail, metric) {
|
|
606
|
+
const t = String(tail || "").trim().replace(/[?.!]+$/, "").trim();
|
|
607
|
+
if (!t) return null;
|
|
608
|
+
const haveM = t.match(/^(?:does|do|did)\s+(.+?)\s+(?:have|has|had|got)$/i);
|
|
609
|
+
if (haveM && haveM[1].trim()) return haveM[1].trim();
|
|
610
|
+
if (metric.kind !== "*" && RELATIONS[metric.kind]) {
|
|
611
|
+
const verbs = [...RELATIONS[metric.kind].verbs].sort((a, b) => b.length - a.length);
|
|
612
|
+
for (const v of verbs) {
|
|
613
|
+
const re = new RegExp(`^${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+(.+)$`, "i");
|
|
614
|
+
const m = t.match(re);
|
|
615
|
+
if (m && m[1].trim()) return m[1].trim();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Bare "how many <edge-noun> <verb> <entity>" / "how many <edge-noun> does
|
|
622
|
+
* <entity> have" (HANDOVER "bare 'how many X' fails for edge-nominalized
|
|
623
|
+
* nouns", 2026-07-12 fix) — "how many tests cover X", "how many importers
|
|
624
|
+
* does X have", "how many callers does X have". answerCount's own COUNT_NOUNS
|
|
625
|
+
* table only maps a counted noun to a graph INDIVIDUAL CLASS (Module/Class/…);
|
|
626
|
+
* an edge-nominalized noun like "tests"/"importers"/"callers" names an EDGE
|
|
627
|
+
* KIND instead — ask-vocab.mjs's EDGE_NOUN_TO_METRIC, the SAME table the
|
|
628
|
+
* working superlative lane ("which module has the most tests") already reads
|
|
629
|
+
* — so it was never in COUNT_NOUNS and answerCount short-circuited straight
|
|
630
|
+
* to "I can't count 'tests'" without ever consulting that table. This reuses
|
|
631
|
+
* the SAME per-entity degree computation the superlative lane's own
|
|
632
|
+
* evalSuperlative uses to rank every entity of a class (ask.mjs's
|
|
633
|
+
* degreeMetric, exported for exactly this) — just read for the ONE named
|
|
634
|
+
* entity instead of sorting all of them.
|
|
635
|
+
*
|
|
636
|
+
* Checked BEFORE answerCount in runTurn (same precedence pattern as
|
|
637
|
+
* answerMemoryCount/answerQuantifierRecall above) so it gets first look;
|
|
638
|
+
* declines (returns null, letting answerCount's existing message stand)
|
|
639
|
+
* whenever:
|
|
640
|
+
* - the noun isn't edge-nominalized at all (a COUNT_NOUNS class, or truly
|
|
641
|
+
* unknown), or
|
|
642
|
+
* - no entity term could be extracted from the tail
|
|
643
|
+
* (extractEdgeCountEntity), or
|
|
644
|
+
* - the extracted term doesn't resolve to exactly one graph entity.
|
|
645
|
+
*
|
|
646
|
+
* SCOPED to the per-entity case only: a bare "how many tests are there" (no
|
|
647
|
+
* named entity in the tail) has nothing for extractEdgeCountEntity to pull
|
|
648
|
+
* out, so it declines here and keeps answerCount's existing "I can't count
|
|
649
|
+
* 'tests'" honest miss — what a GLOBAL edge count would even mean (every
|
|
650
|
+
* test edge in the graph? distinct test modules? distinct tested modules?)
|
|
651
|
+
* is a genuine, undecided design question, out of this fix's scope. */
|
|
652
|
+
async function answerEdgeCount(graph, query) {
|
|
653
|
+
if (!graph) return null;
|
|
654
|
+
const q = String(query);
|
|
655
|
+
if (ANAPHORA_COUNT_RE.test(q) || IMPLICIT_ANAPHORA_COUNT_RE.test(q.trim())) return null;
|
|
656
|
+
const m = q.match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b/i);
|
|
657
|
+
if (!m) return null;
|
|
658
|
+
const noun = m[1].toLowerCase();
|
|
659
|
+
if (COUNT_NOUNS[noun]) return null; // a real graph class — answerCount owns it
|
|
660
|
+
const metric = EDGE_NOUN_TO_METRIC[noun];
|
|
661
|
+
if (!metric) return null; // not edge-nominalized either — answerCount's "I can't count" stands
|
|
662
|
+
const term = extractEdgeCountEntity(q.slice(m.index + m[0].length), metric);
|
|
663
|
+
if (!term) return null; // no per-entity phrasing recognized — scoped out (see docblock)
|
|
664
|
+
const entity = await resolveEntity(graph, term);
|
|
665
|
+
if (!entity) return null; // unresolved/ambiguous entity — honest decline, not a guess
|
|
666
|
+
const ind = graph.byId?.get?.(entity.id);
|
|
667
|
+
if (!ind) return null;
|
|
668
|
+
let degreeMetric;
|
|
669
|
+
try { ({ degreeMetric } = await import("./ask.mjs")); } catch { return null; }
|
|
670
|
+
const n = degreeMetric(graph, ind, metric);
|
|
671
|
+
return `${n} ${edgeCountNoun(noun, n)}.`;
|
|
672
|
+
}
|
|
673
|
+
|
|
567
674
|
/** ASSERTED-VOCABULARY count (CHATBENCH_006 lever 3): once "every class is a type"
|
|
568
675
|
* is remembered, "how many types are there" counts as many types as there are
|
|
569
676
|
* classes — the asserted object noun inherits the subject class's cardinality.
|
|
@@ -869,6 +976,29 @@ export function isConversational(query) {
|
|
|
869
976
|
return q.split(/\s+/).filter(Boolean).length <= 3 && !codeish;
|
|
870
977
|
}
|
|
871
978
|
|
|
979
|
+
/** Scoped exemption for the bare-meta-fact lane (2b/2c, further down this file)
|
|
980
|
+
* ONLY — never a change to looksCodeish()/isConversational() themselves, and
|
|
981
|
+
* never used for the generic orientation-card fallback. HANDOVER.md 2026-07-12
|
|
982
|
+
* finding: a bare "what is TaskController?" (CamelCase COMPOUND class name, no
|
|
983
|
+
* article) hits looksCodeish()'s `/[a-z][A-Z]/` branch, so isConversational()
|
|
984
|
+
* returns false and the whole isConversationalCandidate gate — including the
|
|
985
|
+
* bare-meta-fact lookup that "what is a TaskController" (articled, via its own
|
|
986
|
+
* T5 structural parse) already resolves through — never runs. Single-word/
|
|
987
|
+
* lowercased names ("Widget", "taskcontroller") have no lowercase-to-uppercase
|
|
988
|
+
* transition so they were never affected; only two-word-shaped compounds were.
|
|
989
|
+
* This re-tests the SAME non-CamelCase codeish reasons (paths, dotted refs,
|
|
990
|
+
* `()` calls, STRUCT_WORDS) looksCodeish already covers, so a genuine near-miss
|
|
991
|
+
* structural question ("what is foo.bar()", "what is import") is UNCHANGED —
|
|
992
|
+
* still excluded here, still falls through to its existing (better) miss
|
|
993
|
+
* handling, never the friendly orientation card. */
|
|
994
|
+
function isBareCamelCaseMetaQuestion(query) {
|
|
995
|
+
const raw = String(query).trim();
|
|
996
|
+
const q = raw.toLowerCase().replace(/[.!?]+$/, "").trim();
|
|
997
|
+
const nonCamelCodeish = /[_./]|\(\)/.test(raw) || q.split(/\s+/).some((w) => STRUCT_WORDS.has(w));
|
|
998
|
+
if (nonCamelCodeish || q.split(/\s+/).filter(Boolean).length > 3) return false;
|
|
999
|
+
return BARE_WHATIS_RE.test(raw) || IS_ADJECTIVE_YESNO_RE.test(raw);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
872
1002
|
// ---- the response-template library (W1: templates → render path) ----
|
|
873
1003
|
// The WORDING of the conversational/orientation surfaces lives in
|
|
874
1004
|
// data/templates/responses.jsonl (corpus/templates.mjs) — the template library is
|
|
@@ -3076,7 +3206,17 @@ async function memorySummary(memoryDir, graph) {
|
|
|
3076
3206
|
// both already exist. CASE-PRESERVING: module paths/symbol names are
|
|
3077
3207
|
// case-sensitive, so this reads the ORIGINAL query text, never metaLane's
|
|
3078
3208
|
// lowercased `q` (authorLane's same discipline, just above/below).
|
|
3079
|
-
|
|
3209
|
+
/** A trailing intensifier/filler adverb tacked onto "do"/"does" ("what does the
|
|
3210
|
+
* store module do exactly?", "...do exactly", "what X does really") — fast
|
|
3211
|
+
* loop round 8 (ESL/filler-phrasing angle): the closed-form anchor below used
|
|
3212
|
+
* to require "do"/"does" to be the LAST word before the optional "?", so this
|
|
3213
|
+
* one extra word past it hit the raw grammar wall even though the shared
|
|
3214
|
+
* FILLER_WORDS/normalizeQuery pass (used elsewhere in the file) never sees
|
|
3215
|
+
* this lane's case-preserving text at all. Mirrors MODULE_ORIENT_POLITENESS_RE
|
|
3216
|
+
* just below: closed, optional, single-lane blast radius — a bare "what does
|
|
3217
|
+
* X do" still matches with this suffix empty. */
|
|
3218
|
+
const TRAILING_ADVERB_RE = "(?:\\s+(?:exactly|really|actually|anyway))?";
|
|
3219
|
+
const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)\\s+do${TRAILING_ADVERB_RE}\\??$`, "i");
|
|
3080
3220
|
/** The SUBJECT-FIRST word order of the SAME question ("what saveStore does" vs
|
|
3081
3221
|
* "what does saveStore do") — Tier 6 playtest, §3b surface-variation axis: a
|
|
3082
3222
|
* perfectly natural alternate phrasing of an ALREADY-recognized intent that
|
|
@@ -3086,7 +3226,7 @@ const MODULE_ORIENT_RE = /^what\s+does\s+(.+?)\s+do\??$/i;
|
|
|
3086
3226
|
* UNIQUE graph entity or this lane declines) is what keeps this loose an
|
|
3087
3227
|
* ending safe — a syntactic match against a term that isn't a real entity
|
|
3088
3228
|
* simply falls through unchanged, same as every other lane in this file. */
|
|
3089
|
-
const MODULE_ORIENT_SVO_RE =
|
|
3229
|
+
const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB_RE}\\??$`, "i");
|
|
3090
3230
|
// Seonix Batch 3 (3a) — purpose/identity phrasing: "whats X for"/"what's X
|
|
3091
3231
|
// about"/"what is X for", the sibling of "what does X do" that asks for the
|
|
3092
3232
|
// SAME module-grain overview. Deliberately does NOT claim the literal noun
|
|
@@ -7209,7 +7349,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7209
7349
|
} catch { /* leave false — the ordinary path decides */ }
|
|
7210
7350
|
}
|
|
7211
7351
|
}
|
|
7212
|
-
const
|
|
7352
|
+
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus;
|
|
7353
|
+
const isConversationalCandidate = conversationalCandidateBaseGate && isConversational(query);
|
|
7213
7354
|
// BUG 2 fix (2026-07-09): "what is X" with NO article ("what is john") is BOTH
|
|
7214
7355
|
// conversational-shaped (≤3 words, no code-ish token — isConversational() would
|
|
7215
7356
|
// claim it) AND a legitimate bare meta/fact-lookup form (BARE_WHATIS_RE —
|
|
@@ -7240,8 +7381,20 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7240
7381
|
// chance to run before the orientation card claims the turn.
|
|
7241
7382
|
const bareWhatisShape = BARE_WHATIS_RE.test(String(query).trim());
|
|
7242
7383
|
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(String(query).trim());
|
|
7384
|
+
// HANDOVER.md 2026-07-12 CamelCase finding: `isBareCamelCaseMetaQuestion` (see
|
|
7385
|
+
// its own docblock, above isConversational) OR's in alongside
|
|
7386
|
+
// isConversationalCandidate for THIS lane only — a bare "what is TaskController"
|
|
7387
|
+
// (CamelCase compound, no article) is otherwise excluded solely because
|
|
7388
|
+
// isConversational()'s codeish check fires on the CamelCase transition, even
|
|
7389
|
+
// though every other precondition (miss, no structural parse, no continuation
|
|
7390
|
+
// in flight) already holds. Shares the SAME base gate as isConversationalCandidate
|
|
7391
|
+
// (conversationalCandidateBaseGate) so it's never looser. Scoped to this `if`
|
|
7392
|
+
// alone: the `else if (isConversationalCandidate)` orientation-card fallback
|
|
7393
|
+
// further down is UNCHANGED, so a CamelCase term with no real hit still falls
|
|
7394
|
+
// through to its existing miss handling, never the generic orientation card.
|
|
7395
|
+
const isBareCamelCaseWhatisCandidate = conversationalCandidateBaseGate && isBareCamelCaseMetaQuestion(query);
|
|
7243
7396
|
let bareMetaHit = null;
|
|
7244
|
-
if (isConversationalCandidate && (bareWhatisShape || isAdjectiveShape)) {
|
|
7397
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape)) {
|
|
7245
7398
|
if (memoryDir) {
|
|
7246
7399
|
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle))
|
|
7247
7400
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
@@ -8282,6 +8435,19 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8282
8435
|
return withLast(plainTurn(workingLine, quantifierRecall, { via: "fact", focus }), "recall a taught quantifier");
|
|
8283
8436
|
}
|
|
8284
8437
|
}
|
|
8438
|
+
// Edge-nominalized "how many X" counts ("how many tests cover Y", "how many
|
|
8439
|
+
// callers does Y have") — checked BEFORE answerCount (same precedence
|
|
8440
|
+
// pattern as answerQuantifierRecall/answerMemoryCount above): answerCount's
|
|
8441
|
+
// own COUNT_NOUNS table doesn't know these nouns at all and would otherwise
|
|
8442
|
+
// short-circuit straight to "I can't count 'tests'" before this lane ever
|
|
8443
|
+
// got a turn. Declines (null) for anything answerCount should own, or a bare
|
|
8444
|
+
// global count with no named entity — see answerEdgeCount's own docblock.
|
|
8445
|
+
const edgeCount = await answerEdgeCount(graph, workingLine);
|
|
8446
|
+
if (edgeCount != null) {
|
|
8447
|
+
note(trace, 'goal: get a per-entity count of an edge-nominalized kind ("how many tests cover X", "how many callers does X have")');
|
|
8448
|
+
note(trace, "lane: answerEdgeCount — matched an EDGE_NOUN_TO_METRIC noun with a resolvable named entity, answered via the same degreeMetric the superlative lane uses");
|
|
8449
|
+
return withLast(plainTurn(workingLine, edgeCount, { via: "count", focus }), "get a per-entity edge count");
|
|
8450
|
+
}
|
|
8285
8451
|
// Aggregate/count questions are answered mechanically off the loaded graph header,
|
|
8286
8452
|
// BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
|
|
8287
8453
|
const count = answerCount(graph, workingLine);
|