@polycode-projects/the-mechanical-code-talker 1.0.3 → 1.0.5
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/package.json +1 -1
- package/src/ask.mjs +220 -13
- package/src/chat.mjs +41 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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
|
@@ -291,6 +291,14 @@ const NEST_SENTINEL = "zzinnerset";
|
|
|
291
291
|
// then"/"what about X though".
|
|
292
292
|
const PRED_LEAD_SKIP = new Set(["that", "which", "who", "are", "is", "was", "were", "do", "does", "also", "still", "both", "and", "then", "though"]);
|
|
293
293
|
const FRAME_WORDS = new Set(["which", "what", "who", "list", "show", "find", "give", "me", "us", "all"]);
|
|
294
|
+
// A bare copula leading a boolean branch ("...and ARE untested") is discourse glue,
|
|
295
|
+
// not part of the qualifier — dropped before a branch is tested/used as a
|
|
296
|
+
// qualifier-only atom (both the marker-gate probe below and the two atom-building
|
|
297
|
+
// folds — buildPredicateAtoms, parseRelationalOrQualified's own fold — apply it).
|
|
298
|
+
const COPULA_WORDS = new Set(["are", "is", "was", "were"]);
|
|
299
|
+
function dropLeadCopula(bw, blc) {
|
|
300
|
+
return blc.length && COPULA_WORDS.has(blc[0]) ? { bw: bw.slice(1), blc: blc.slice(1) } : { bw, blc };
|
|
301
|
+
}
|
|
294
302
|
|
|
295
303
|
const entityNoun = (w) => (ENTITY_TO_TYPE[w] ? { entityType: ENTITY_TO_TYPE[w], placeholder: false }
|
|
296
304
|
: (PLACEHOLDER_NOUNS.includes(w) ? { entityType: null, placeholder: true } : null));
|
|
@@ -311,7 +319,8 @@ function parseSimpleClause(text, nlp) {
|
|
|
311
319
|
function parseComposite(text, nlp) {
|
|
312
320
|
const w = splitWords(text);
|
|
313
321
|
const lc = w.map((x) => x.toLowerCase());
|
|
314
|
-
return
|
|
322
|
+
return parseExistence(w, lc)
|
|
323
|
+
|| parseNegation(text, nlp, 0)
|
|
315
324
|
|| parseForwardNegation(w, lc, nlp)
|
|
316
325
|
|| parseTemporal(w, lc, nlp, 0)
|
|
317
326
|
|| parseAnaphora(w, lc, nlp)
|
|
@@ -580,6 +589,72 @@ function parsePredicateFilter(words, nlp) {
|
|
|
580
589
|
return undefined;
|
|
581
590
|
}
|
|
582
591
|
|
|
592
|
+
/** EXISTENCE: "is there a/an <kind> [called|named <term>] [in <module>] [anywhere]"
|
|
593
|
+
* and "are there any <kind>(s) [called|named <term>] [in <module>]" — a genuine
|
|
594
|
+
* existence question ("does this kind/name exist at all", optionally scoped to a
|
|
595
|
+
* module), answered directly against class/kind membership rather than routed
|
|
596
|
+
* through the relation-verb machinery. Triage bug (2026-07-09, seonix dogfooding):
|
|
597
|
+
* with no dedicated recognizer, "is there a class called Store anywhere" fell
|
|
598
|
+
* through to the legacy keyword-spot strategy, whose lemma tier canonicalizes
|
|
599
|
+
* "called" -> "call" (a `calls` verb — ask-vocab.mjs) and silently answered a
|
|
600
|
+
* DIFFERENT question ("which classes call Store") with a confidently-wrong-shaped
|
|
601
|
+
* negative, even though a class named Store genuinely exists. "is there a class in
|
|
602
|
+
* <module>" walled out the same way — no marker in this grammar recognized it at
|
|
603
|
+
* all. Scoped to a tight closed shape: a leading "is there a/an" or "are there any"
|
|
604
|
+
* immediately followed by a recognized entity-kind noun, then ONLY "called"/"named
|
|
605
|
+
* <term>", "in <module>", the two combined, or an empty/"anywhere"/"at all" tail —
|
|
606
|
+
* anything else (a relative clause, a verb phrase: "is there a class THAT CALLS
|
|
607
|
+
* Store") is a genuine relationship question and is left untouched for the
|
|
608
|
+
* relation parsers below, never swallowed here. */
|
|
609
|
+
function parseExistence(w, lc) {
|
|
610
|
+
let i;
|
|
611
|
+
if (lc[0] === "is" && lc[1] === "there") i = 2;
|
|
612
|
+
else if (lc[0] === "are" && lc[1] === "there") i = 2;
|
|
613
|
+
else return null;
|
|
614
|
+
const article = lc[i];
|
|
615
|
+
if (article === "a" || article === "an" || article === "any") i += 1;
|
|
616
|
+
else return null;
|
|
617
|
+
const noun = i < lc.length ? entityNoun(lc[i]) : null;
|
|
618
|
+
if (!noun || noun.placeholder || !noun.entityType) return null;
|
|
619
|
+
const entityType = noun.entityType;
|
|
620
|
+
i += 1;
|
|
621
|
+
|
|
622
|
+
let rest = lc.slice(i);
|
|
623
|
+
let restW = w.slice(i);
|
|
624
|
+
// trailing filler — "anywhere" / "at all" — stripped so it never gets misread as
|
|
625
|
+
// a (nonexistent) module/name term below.
|
|
626
|
+
if (rest.length && rest[rest.length - 1] === "anywhere") {
|
|
627
|
+
rest = rest.slice(0, -1); restW = restW.slice(0, -1);
|
|
628
|
+
} else if (rest.length >= 2 && rest[rest.length - 2] === "at" && rest[rest.length - 1] === "all") {
|
|
629
|
+
rest = rest.slice(0, -2); restW = restW.slice(0, -2);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!rest.length) return { node: "exists", entityType, term: null, scopeModule: null };
|
|
633
|
+
|
|
634
|
+
if (rest[0] === "called" || rest[0] === "named") {
|
|
635
|
+
if (rest.length < 2) return { node: "miss", reason: `"${rest[0]}" needs a name afterward` };
|
|
636
|
+
const inIdx = rest.indexOf("in", 1);
|
|
637
|
+
if (inIdx > 0) {
|
|
638
|
+
const term = restW.slice(1, inIdx).join(" ").trim();
|
|
639
|
+
const scopeModule = restW.slice(inIdx + 1).join(" ").trim();
|
|
640
|
+
if (!term || !scopeModule) return { node: "miss", reason: `a named existence check needs both a name and a module after "in"` };
|
|
641
|
+
return { node: "exists", entityType, term, scopeModule };
|
|
642
|
+
}
|
|
643
|
+
const term = restW.slice(1).join(" ").trim();
|
|
644
|
+
return term ? { node: "exists", entityType, term, scopeModule: null }
|
|
645
|
+
: { node: "miss", reason: `"${rest[0]}" needs a name afterward` };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (rest[0] === "in") {
|
|
649
|
+
const scopeModule = restW.slice(1).join(" ").trim();
|
|
650
|
+
return scopeModule ? { node: "exists", entityType, term: null, scopeModule }
|
|
651
|
+
: { node: "miss", reason: `"in" needs a module afterward` };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return null; // a relative clause / verb phrase / anything else — genuinely a
|
|
655
|
+
// different (relationship) question; leave it for the parsers below.
|
|
656
|
+
}
|
|
657
|
+
|
|
583
658
|
/** Trailing "and that's the whole question" filler an aggregate/list tail can carry
|
|
584
659
|
* ("how many classes are there", "list functions in total", "which classes exist in
|
|
585
660
|
* the index") — a count/list over a bare kind is frequently phrased with such a tail,
|
|
@@ -863,7 +938,8 @@ function buildPredicateAtoms(entityType, subjPrefix, predLc, predWords, nlp, dep
|
|
|
863
938
|
const bw = branches[b];
|
|
864
939
|
const blc = bw.map((x) => x.toLowerCase());
|
|
865
940
|
const op = b === 0 ? "intersection" : ops[b - 1];
|
|
866
|
-
|
|
941
|
+
const qc = dropLeadCopula(bw, blc);
|
|
942
|
+
if (qc.blc.length && qc.blc.every((x) => QUALIFIERS[x])) { atoms.push({ op, kind: "qual", filters: qc.blc }); continue; }
|
|
867
943
|
if (blc[0] === "of" || blc[0] === "in") {
|
|
868
944
|
atoms.push({ op, kind: "set", ast: { node: "membership", entityType, term: bw.slice(1).join(" ") } });
|
|
869
945
|
continue;
|
|
@@ -938,9 +1014,20 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
|
938
1014
|
if (predLc.length && RELATIVE_PRONOUNS.includes(predLc[0])) { relFlag = true; predLc = predLc.slice(1); predWords = predWords.slice(1); }
|
|
939
1015
|
const membershipLed = predLc[0] === "of" || predLc[0] === "in";
|
|
940
1016
|
const gerundLed = predLc.length > 0 && isGerundVerb(predLc[0]);
|
|
1017
|
+
// A boolean branch whose OWN content — past an optional leading copula ("and ARE
|
|
1018
|
+
// untested") — collapses to qualifier words alone is the compositional shape too
|
|
1019
|
+
// ("functions that call X and are untested": verb clause AND qualifier, same
|
|
1020
|
+
// subject). Probe with the same splitBoolean+QUALIFIERS fold the atoms loop below
|
|
1021
|
+
// uses, so a bare verb+verb boolean chain with NO qualifier signal anywhere
|
|
1022
|
+
// ("which classes extends Base and couples to logging") still has no marker here
|
|
1023
|
+
// and correctly stays on the legacy ambiguous-parse path, untouched.
|
|
1024
|
+
const boolQualLed = predWords.length > 0 && splitBoolean(predLc, predWords).branches.some((bw) => {
|
|
1025
|
+
const { blc } = dropLeadCopula(bw, bw.map((x) => x.toLowerCase()));
|
|
1026
|
+
return blc.length && blc.every((x) => QUALIFIERS[x]);
|
|
1027
|
+
});
|
|
941
1028
|
// marker gate — the crux of backward-compat: without one of these, this is not a
|
|
942
1029
|
// compositional query and we must NOT hijack it from the existing parser.
|
|
943
|
-
if (!(quals.length || relFlag || membershipLed || gerundLed)) return null;
|
|
1030
|
+
if (!(quals.length || relFlag || membershipLed || gerundLed || boolQualLed)) return null;
|
|
944
1031
|
|
|
945
1032
|
// empty predicate → a bare qualified class ("public methods")
|
|
946
1033
|
if (!predWords.length) {
|
|
@@ -959,7 +1046,8 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
|
959
1046
|
const bw = branches[b];
|
|
960
1047
|
const blc = bw.map((x) => x.toLowerCase());
|
|
961
1048
|
const op = b === 0 ? "seed" : ops[b - 1];
|
|
962
|
-
|
|
1049
|
+
const qc = dropLeadCopula(bw, blc);
|
|
1050
|
+
if (qc.blc.length && qc.blc.every((x) => QUALIFIERS[x])) { atoms.push({ op, kind: "qual", filters: qc.blc }); continue; }
|
|
963
1051
|
if (blc[0] === "of" || blc[0] === "in") {
|
|
964
1052
|
atoms.push({ op, kind: "set", ast: { node: "membership", entityType, term: bw.slice(1).join(" ") } });
|
|
965
1053
|
continue;
|
|
@@ -1479,10 +1567,42 @@ function evalSuperlative(graph, ast) {
|
|
|
1479
1567
|
return { compositeKind: "superlative", entityType: ast.entityType, metricNoun: ast.metricNoun, extreme: ast.extreme, score: best, matches: winners };
|
|
1480
1568
|
}
|
|
1481
1569
|
|
|
1570
|
+
/** EXISTENCE eval — "is there a/an <kind> [called/named <term>] [in <module>]": a
|
|
1571
|
+
* direct membership/name check against the graph, never routed through the
|
|
1572
|
+
* relation-verb machinery. A named check resolves the term against the SAME
|
|
1573
|
+
* tiered resolveObject() every other named-lookup shape uses (expectedClass pins
|
|
1574
|
+
* the pool to the asked kind, so "is there a class called Store" can never
|
|
1575
|
+
* resolve to a same-named function/module); a scope clause resolves the module
|
|
1576
|
+
* the same way and narrows the check to that module's own `defines` edges
|
|
1577
|
+
* (refineToEntities — the same primitive members-of-a-module questions use). */
|
|
1578
|
+
function evalExists(graph, ast) {
|
|
1579
|
+
const { entityType, term, scopeModule } = ast;
|
|
1580
|
+
let scopeMatch = null;
|
|
1581
|
+
if (scopeModule) {
|
|
1582
|
+
const r = resolveObject(graph, scopeModule, { expectedClass: "Module" });
|
|
1583
|
+
if (!r.match) return { compositeKind: "exists", entityType, term, scopeModule, scopeMiss: true, matches: [] };
|
|
1584
|
+
scopeMatch = r.match;
|
|
1585
|
+
}
|
|
1586
|
+
if (term) {
|
|
1587
|
+
const r = resolveObject(graph, term, { expectedClass: entityType });
|
|
1588
|
+
const inScope = !scopeMatch || (r.match && moduleIdOf(graph, r.match) === scopeMatch.id);
|
|
1589
|
+
const hit = r.match && inScope;
|
|
1590
|
+
return {
|
|
1591
|
+
compositeKind: "exists", entityType, term, scopeModule, scopeMatch,
|
|
1592
|
+
matches: hit ? [r.match] : [],
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
const pool = scopeMatch
|
|
1596
|
+
? refineToEntities(graph, new Set([scopeMatch.id]), entityType)
|
|
1597
|
+
: graph.individuals.filter((i) => i.class === entityType);
|
|
1598
|
+
return { compositeKind: "exists", entityType, term: null, scopeModule, scopeMatch, matches: pool };
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1482
1601
|
/** Compile any compositional AST to a result object traverse() returns for the
|
|
1483
1602
|
* simple path — {matches, …} plus compositeKind/compositeMiss flags render() reads. */
|
|
1484
1603
|
export function evalComposite(graph, ast, opts = {}) {
|
|
1485
1604
|
if (ast.node === "miss") return { compositeMiss: true, reason: ast.reason || null, matches: [] };
|
|
1605
|
+
if (ast.node === "exists") return evalExists(graph, ast);
|
|
1486
1606
|
if (ast.node === "count") return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
|
|
1487
1607
|
if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
|
|
1488
1608
|
if (ast.node === "superlative") return evalSuperlative(graph, ast);
|
|
@@ -1532,6 +1652,30 @@ function renderComposite(parsed, result) {
|
|
|
1532
1652
|
}
|
|
1533
1653
|
return { content: `couldn't compile this compositional question${result.reason ? ` (${result.reason})` : ""}. ${compositionalHint()}.`, miss: true, ambiguous: false };
|
|
1534
1654
|
}
|
|
1655
|
+
// exists: "is there a/an <kind> [called/named <term>] [in <module>]" — an
|
|
1656
|
+
// honest Yes/No membership check, never routed through the relation-verb
|
|
1657
|
+
// machinery (see parseExistence's own doc for the bug this fixes).
|
|
1658
|
+
if (result.compositeKind === "exists") {
|
|
1659
|
+
if (result.scopeMiss) {
|
|
1660
|
+
return { content: `no module matching "${result.scopeModule}" found in the index.`, miss: true, ambiguous: false };
|
|
1661
|
+
}
|
|
1662
|
+
const kindSingular = nounFor(result.entityType, 1);
|
|
1663
|
+
const kindPlural = nounFor(result.entityType, 2);
|
|
1664
|
+
const scopeSuffix = result.scopeMatch ? ` in ${result.scopeMatch.label}` : "";
|
|
1665
|
+
if (result.term) {
|
|
1666
|
+
if (!result.matches.length) {
|
|
1667
|
+
return { content: `No — no ${kindSingular} named "${result.term}" found${scopeSuffix}.`, miss: true, ambiguous: false };
|
|
1668
|
+
}
|
|
1669
|
+
const hit = result.matches[0];
|
|
1670
|
+
const modLabel = moduleLabelOf(hit);
|
|
1671
|
+
const definedIn = hit.class === "Module" ? "" : (modLabel && modLabel !== "(unknown module)" ? `, defined in ${modLabel}` : "");
|
|
1672
|
+
return { content: `Yes — ${hit.label} is a ${kindSingular}${definedIn}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1673
|
+
}
|
|
1674
|
+
if (!result.matches.length) {
|
|
1675
|
+
return { content: `No — no ${kindPlural} found${scopeSuffix}.`, miss: true, ambiguous: false };
|
|
1676
|
+
}
|
|
1677
|
+
return { content: `Yes — ${compositeList(result.matches)}${scopeSuffix}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
1678
|
+
}
|
|
1535
1679
|
if (result.compositeKind === "count") {
|
|
1536
1680
|
const noun = result.entityType ? nounFor(result.entityType, result.count) : (result.count === 1 ? "result" : "results");
|
|
1537
1681
|
return { content: `${result.count} ${noun}.`, miss: false, ambiguous: false, matches: [] };
|
|
@@ -1738,15 +1882,66 @@ export function resolveObject(graph, term, { expectedClass = null } = {}) {
|
|
|
1738
1882
|
}
|
|
1739
1883
|
}
|
|
1740
1884
|
} else {
|
|
1741
|
-
|
|
1885
|
+
// Root-cause fix (Tier-2 playtest cycle 9, targeted substring-match sweep):
|
|
1886
|
+
// this raw containment check has no minimum-length floor, so a short
|
|
1887
|
+
// closed-vocabulary word is a near-certain ACCIDENTAL substring of SOME
|
|
1888
|
+
// real label — confirmed empirically against the shipped mini-webapp
|
|
1889
|
+
// fixture: "so"->sendJson, "or"->Store, "a"->Task, "is"->listTasks
|
|
1890
|
+
// (ambiguous), "in"->Logger.info, "on"->sendJson, "at"->createApp,
|
|
1891
|
+
// "to"->Store, all via this exact branch. Cycle 8 patched three of these
|
|
1892
|
+
// ("and"/"also"/"so"/"then"/"now" and bare "it") one word at a time at
|
|
1893
|
+
// individual chat.mjs CALL SITES (STACCATO_LEAKED_CONNECTIVES, the
|
|
1894
|
+
// pronoun-reuse guard) — necessary there because those bugs are about
|
|
1895
|
+
// FOCUS bookkeeping, not resolution per se, but leaving resolveObject
|
|
1896
|
+
// itself unguarded meant every OTHER caller (existence checks, expectedClass
|
|
1897
|
+
// lookups, etc.) stayed exposed to the same trap for every not-yet-hit
|
|
1898
|
+
// short word. Gating the containment check at the same floor tier 5's own
|
|
1899
|
+
// fuzzy pass already uses (`tLc.length >= 4` below) closes it at the
|
|
1900
|
+
// source. A whole-token component match (just below) is unaffected by this
|
|
1901
|
+
// floor — it requires an EXACT path/identifier segment equality, never a
|
|
1902
|
+
// raw substring, so a genuinely short real identifier ("db", "fs") is
|
|
1903
|
+
// still resolvable by literally matching a whole segment.
|
|
1904
|
+
const termComps = [...componentSet(t)];
|
|
1905
|
+
// A SLASHED term's final path segment, extension stripped ("src/nope.mjs"
|
|
1906
|
+
// -> "nope", "cover app/lib/b.mjs" -> "b") — the semantically load-bearing
|
|
1907
|
+
// FILENAME STEM, as opposed to a directory segment or a leaked verb noise
|
|
1908
|
+
// word. Isolated from the actual whitespace-delimited PATH TOKEN (not the
|
|
1909
|
+
// raw multi-word string as a whole) — "app/lib/f.mjs but untested" (a
|
|
1910
|
+
// trailing-noise leak, distinct from the leading-verb-noise shape above)
|
|
1911
|
+
// has no extension at the end of the whole string, so splitting the whole
|
|
1912
|
+
// string would strand "f.mjs but untested" as a bogus non-matching "stem";
|
|
1913
|
+
// finding the one token that itself contains "/" keeps the path term
|
|
1914
|
+
// intact regardless of what noise surrounds it on either side. Only
|
|
1915
|
+
// slash-shaped terms compute this; a bare identifier/multi-word query has
|
|
1916
|
+
// no path structure to anchor on, so it's null and the gate below is a
|
|
1917
|
+
// no-op for those (unaffected — original ANY-overlap behavior).
|
|
1918
|
+
const pathToken = tLc.split(/\s+/).find((tok) => tok.includes("/"));
|
|
1919
|
+
const slashStem = pathToken ? pathToken.split("/").pop().replace(/\.[a-z0-9]+$/, "") : null;
|
|
1742
1920
|
for (const m of pool) {
|
|
1743
1921
|
const label = String(m.label || "").toLowerCase();
|
|
1744
|
-
if (label.includes(tLc)) {
|
|
1922
|
+
if (tLc.length >= 4 && label.includes(tLc)) {
|
|
1745
1923
|
scored.push({ ind: m, score: 1000 - Math.abs(label.length - tLc.length) });
|
|
1746
1924
|
continue;
|
|
1747
1925
|
}
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1926
|
+
const labelComps = componentSet(m.label);
|
|
1927
|
+
const overlap = termComps.filter((c) => labelComps.has(c)).length;
|
|
1928
|
+
// For a slashed term, the stem MUST be among the label's own components
|
|
1929
|
+
// — an ANY-overlap match otherwise lets a NONEXISTENT path land on a
|
|
1930
|
+
// real module that merely shares its generic directory/extension
|
|
1931
|
+
// segments ("src", "mjs") with every other module in the pool (found
|
|
1932
|
+
// via the existence recognizer's "is there a class in src/nope.mjs"
|
|
1933
|
+
// scope clause, Tier-2 playtest cycle 9: it ambiguously "matched"
|
|
1934
|
+
// src/core/model.mjs even though no such module exists — the same
|
|
1935
|
+
// accidental-match disease as the short-word substring bug just above,
|
|
1936
|
+
// just triggered by GENERIC components instead of raw containment).
|
|
1937
|
+
// A leaked leading VERB ("cover app/lib/b.mjs", "touch app/lib/f.mjs" —
|
|
1938
|
+
// router-interface.test.mjs's own frozen contract) still resolves: the
|
|
1939
|
+
// stem ("b"/"f") is a real component of the target label even though
|
|
1940
|
+
// "cover"/"touch" themselves never overlap anything, so overlap>0 and
|
|
1941
|
+
// the stem gate both hold.
|
|
1942
|
+
if (overlap > 0 && (!slashStem || labelComps.has(slashStem))) {
|
|
1943
|
+
scored.push({ ind: m, score: overlap * 10 });
|
|
1944
|
+
}
|
|
1750
1945
|
}
|
|
1751
1946
|
}
|
|
1752
1947
|
scored.sort((a, b) => b.score - a.score);
|
|
@@ -1767,12 +1962,24 @@ export function resolveObject(graph, term, { expectedClass = null } = {}) {
|
|
|
1767
1962
|
// browser `lookupByProseTokens` is an undeclared identifier — without the guard,
|
|
1768
1963
|
// ANY term reaching this tier threw a ReferenceError in the page instead of
|
|
1769
1964
|
// rendering the honest miss (a real, previously-untested viewer bug).
|
|
1770
|
-
// DOTTED terms never consult prose: "res.json" word-matches
|
|
1771
|
-
// own path tokens, which is the tier-3 phantom-path bug
|
|
1772
|
-
// side door — a dotted term names an identifier, and
|
|
1773
|
-
// label (tiers above) or the bounded fuzzy pass
|
|
1965
|
+
// DOTTED or SLASHED terms never consult prose: "res.json" word-matches
|
|
1966
|
+
// test/res.json.js's own path tokens, which is the tier-3 phantom-path bug
|
|
1967
|
+
// reappearing through a side door — a dotted term names an identifier, and
|
|
1968
|
+
// identifiers resolve by label (tiers above) or the bounded fuzzy pass
|
|
1969
|
+
// below, or they honestly miss. The SAME side door was open for SLASHED
|
|
1970
|
+
// path terms too (Tier-2 playtest cycle 9, existence-recognizer follow-up):
|
|
1971
|
+
// "src/nope.mjs" — a nonexistent module — no longer false-matches tier 3
|
|
1972
|
+
// (the AND-across-components fix just above), but fell through to THIS
|
|
1973
|
+
// prose tier and ambiguously "matched" real modules anyway, because
|
|
1974
|
+
// lookupByProseTokens scores by ANY-token overlap (sum-scored, by design,
|
|
1975
|
+
// for genuine prose ranking) and "src"/"mjs" are near-universal path/
|
|
1976
|
+
// extension tokens shared by every module in the pool — the identical
|
|
1977
|
+
// accidental-match disease, just one tier further down. A slash-shaped term
|
|
1978
|
+
// names a literal path exactly like a dotted term names a literal symbol;
|
|
1979
|
+
// neither is prose, so neither should ever reach the prose fallback.
|
|
1980
|
+
const pathShaped = dotted || tLc.includes("/");
|
|
1774
1981
|
let proseResult = null;
|
|
1775
|
-
const proseHits = !
|
|
1982
|
+
const proseHits = !pathShaped && typeof lookupByProseTokens === "function"
|
|
1776
1983
|
? lookupByProseTokens(graph.proseIndex, t).filter((h) => !expectedClass || graph.byId.get(h.id)?.class === expectedClass)
|
|
1777
1984
|
: [];
|
|
1778
1985
|
if (proseHits.length) {
|
package/src/chat.mjs
CHANGED
|
@@ -3300,7 +3300,13 @@ async function describeWrapperAnswer(query, { config, source, focus }) {
|
|
|
3300
3300
|
* known edge concept (RELATION_TERM), no curated definition, or the graph has NO
|
|
3301
3301
|
* edges of that kind (composeRelation's own honest-miss gate). Loads the definition
|
|
3302
3302
|
* from the shipped corpus/seon/relations.jsonl, so it works without per-repo memory
|
|
3303
|
-
* seeding. Lazy + failure-tolerated throughout. Returns { text, pending }
|
|
3303
|
+
* seeding. Lazy + failure-tolerated throughout. Returns { text, pending, kind } —
|
|
3304
|
+
* `kind` is the resolved RELATION_TERM canonical kind (imports/calls/…), the SAME
|
|
3305
|
+
* vocabulary GOAL_BY_KIND keys on, so a caller whose own envelope.parsed never
|
|
3306
|
+
* stood (this force's whole reason to exist — see relationTermOf/
|
|
3307
|
+
* isVagueRelationTouch's own docs) can still deduce the correct "Goal (inferred):
|
|
3308
|
+
* …" line instead of silently carrying forward a null goal from earlier in the
|
|
3309
|
+
* turn. */
|
|
3304
3310
|
async function relationForceAnswer(query, envelope, { graph, config, source, templates }) {
|
|
3305
3311
|
const rawTerm = relationTermOf(query, envelope);
|
|
3306
3312
|
if (!rawTerm) return null;
|
|
@@ -3308,8 +3314,9 @@ async function relationForceAnswer(query, envelope, { graph, config, source, tem
|
|
|
3308
3314
|
try { ({ composeRelation, RELATION_TERM } = await import("./concept.mjs")); }
|
|
3309
3315
|
catch { return null; }
|
|
3310
3316
|
const term = String(rawTerm).toLowerCase();
|
|
3311
|
-
|
|
3312
|
-
|
|
3317
|
+
const kind = RELATION_TERM[term];
|
|
3318
|
+
if (!kind) return null; // not an enumerable edge concept — ordinary path owns it
|
|
3319
|
+
const definition = (await relationDefinitions()).get(kind) ?? null;
|
|
3313
3320
|
if (!definition) return null;
|
|
3314
3321
|
// Same graph-load fallback as conceptForceAnswer: the shell hands the loaded graph
|
|
3315
3322
|
// straight in; the pure runTurn(config) path loads it the way dispatchTool does.
|
|
@@ -3329,7 +3336,7 @@ async function relationForceAnswer(query, envelope, { graph, config, source, tem
|
|
|
3329
3336
|
const pending = composed.remainder && composed.remainder.length
|
|
3330
3337
|
? { items: composed.remainder, noun: composed.noun }
|
|
3331
3338
|
: null;
|
|
3332
|
-
return { text, pending };
|
|
3339
|
+
return { text, pending, kind };
|
|
3333
3340
|
}
|
|
3334
3341
|
|
|
3335
3342
|
/** THE CONCEPT FORCE — compose the three-band answer (corpus/seon definition + real
|
|
@@ -3571,7 +3578,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3571
3578
|
// SAME value the debug trace's own "goal:" line uses (one deduction, two
|
|
3572
3579
|
// presentations) — null here (no parse stood at all) means withGoalLine
|
|
3573
3580
|
// shows nothing, never a "Goal (inferred): unclear" line.
|
|
3574
|
-
const
|
|
3581
|
+
// `let`, not `const`: the RELATION CONCEPT FORCE (relationForceAnswer, below)
|
|
3582
|
+
// can answer a turn CORRECTLY with no envelope.parsed at all to deduce from —
|
|
3583
|
+
// a staccato relation-chain continuation whose leading connective never
|
|
3584
|
+
// itself parses ("and inherits?": ask()'s raw grammar has no production for
|
|
3585
|
+
// a bare relation word with no verb, exactly like the "cochange" vague-touch
|
|
3586
|
+
// gap composeRelation's own degrade fix addressed) still reaches the SAME
|
|
3587
|
+
// relation force a normally-parsed "what about inherits" would. Tier-2
|
|
3588
|
+
// playtest cycle 9, the Goal-line gap cycle 8 flagged: the answer content
|
|
3589
|
+
// was always correct here — only the cosmetic trailing "Goal (inferred): …"
|
|
3590
|
+
// line went missing, because it was computed once, this early, straight off
|
|
3591
|
+
// envelope.parsed and never revisited even when a LATER lane went on to
|
|
3592
|
+
// answer the turn through a completely different path. Reassigned at the
|
|
3593
|
+
// relation-force call site below (never overwritten with something worse:
|
|
3594
|
+
// only filled in from the SAME GOAL_BY_KIND table deduceGoalFromParsed
|
|
3595
|
+
// itself already uses for a normally-parsed relation query, so the two
|
|
3596
|
+
// never disagree on the cases where both would fire).
|
|
3597
|
+
let deduced = deduceGoalFromParsed(envelope?.parsed);
|
|
3575
3598
|
note(trace, `goal: ${deduced ?? "unclear — the phrasing didn't resolve to a known query shape"}`);
|
|
3576
3599
|
// MISS handling. The intent lanes + short-miss are RECOGNIZER-gated on the query
|
|
3577
3600
|
// text AND only consulted on a would-miss, so a real graph query — a hit, an honest
|
|
@@ -3800,6 +3823,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3800
3823
|
conceptPending = relation.pending;
|
|
3801
3824
|
note(trace, "lane: THE RELATION CONCEPT FORCE — the touched word named a known, edge-bearing relation kind");
|
|
3802
3825
|
note(trace, "source: relationForceAnswer over the loaded graph's own edges (not corpus)");
|
|
3826
|
+
// Goal-line gap fix (Tier-2 playtest cycle 9): this force just answered
|
|
3827
|
+
// the turn CORRECTLY over a query shape ask()'s own grammar may never
|
|
3828
|
+
// have parsed at all (a staccato relation-chain continuation whose
|
|
3829
|
+
// leading connective never itself parses, "and inherits?") — `deduced`
|
|
3830
|
+
// was computed way above, off envelope.parsed alone, and would
|
|
3831
|
+
// otherwise stay null forever here even though the answer is real.
|
|
3832
|
+
// relation.kind is the SAME GOAL_BY_KIND vocabulary a normally-parsed
|
|
3833
|
+
// relation query already deduces its goal line from, so this can never
|
|
3834
|
+
// disagree with the ordinary path on a case where both would fire.
|
|
3835
|
+
if (relation.kind && GOAL_BY_KIND[relation.kind]) {
|
|
3836
|
+
deduced = GOAL_BY_KIND[relation.kind];
|
|
3837
|
+
note(trace, `goal: ${deduced} (revised — the relation concept force answered where the raw parse never stood)`);
|
|
3838
|
+
}
|
|
3803
3839
|
}
|
|
3804
3840
|
}
|
|
3805
3841
|
}
|