@polycode-projects/the-mechanical-code-talker 1.0.1 → 1.0.3
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 +3 -2
- package/src/ask.mjs +24 -3
- package/src/chat.mjs +245 -7
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.3",
|
|
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.",
|
|
@@ -89,7 +89,8 @@
|
|
|
89
89
|
"agentbench:run": "node agentbench/run.mjs",
|
|
90
90
|
"infbench": "node infbench/generate-cases.mjs && node infbench/run.mjs",
|
|
91
91
|
"audit": "npm audit --audit-level=high",
|
|
92
|
-
"audit:fix": "npm audit fix"
|
|
92
|
+
"audit:fix": "npm audit fix",
|
|
93
|
+
"demo:build": "node scripts/build-demo-site.mjs"
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|
|
95
96
|
"ink-testing-library": "^4.0.0"
|
package/src/ask.mjs
CHANGED
|
@@ -912,8 +912,19 @@ function parseRelationalOrQualified(w, lc, nlp, depth) {
|
|
|
912
912
|
// auxiliary in that position ("what DID commit X touch") is left for the existing
|
|
913
913
|
// parser, not mistaken for a term.
|
|
914
914
|
const nextNoun = i + 1 < lc.length ? entityNoun(lc[i + 1]) : null;
|
|
915
|
+
// CASCADE_NOISE_SET excluded alongside STOPWORDS (Tier-2 playtest, cycle 8):
|
|
916
|
+
// "what about classes"/"how about the modules" used to reach here with
|
|
917
|
+
// "about" sitting right where a real qualifying adjective would ("payment"
|
|
918
|
+
// in "list payment modules") — framed (past "what") and immediately before
|
|
919
|
+
// a known noun ("classes") — and get misread as a fuzzy find TERM ("no
|
|
920
|
+
// classes found matching 'about'") instead of the topic-lead-in filler it
|
|
921
|
+
// is. CASCADE_NOISE already curates "about" for exactly this reading (see
|
|
922
|
+
// its own docblock, ask-vocab.mjs) — checking it here too lets a real
|
|
923
|
+
// unknown qualifier ("shiny"/"payment") still reach the find fallback while
|
|
924
|
+
// a known no-graph-meaning filler word falls through to the ordinary bare-
|
|
925
|
+
// kind-noun path instead (the cascade's own noise-strip terminal rule).
|
|
915
926
|
if ((framed || quals.length) && nextNoun && /^[a-z]+$/.test(lc[i])
|
|
916
|
-
&& !VERB_TO_KIND[lc[i]] && !STOPWORDS.has(lc[i])) {
|
|
927
|
+
&& !VERB_TO_KIND[lc[i]] && !STOPWORDS.has(lc[i]) && !CASCADE_NOISE_SET.has(lc[i])) {
|
|
917
928
|
return { node: "find", entityType: nextNoun.entityType, term: w[i] };
|
|
918
929
|
}
|
|
919
930
|
return null; // no subject entity → not this shape
|
|
@@ -2408,10 +2419,20 @@ function renderCore(parsed, result) {
|
|
|
2408
2419
|
if (!result.objMatch && (!result.candidates || result.candidates.length === 0) && parsed.shape !== "ask") {
|
|
2409
2420
|
// name what kind of thing was looked for: a sha-shaped term was checked against
|
|
2410
2421
|
// the commit namespace, a dotted slash-free term against symbol labels — a
|
|
2411
|
-
// generic "no module matching" would misreport both.
|
|
2422
|
+
// generic "no module matching" would misreport both. Those two TERM-SHAPE
|
|
2423
|
+
// reads keep priority (a bare "a.mjs" is deliberately read as a symbol-ish
|
|
2424
|
+
// shorthand regardless of the stated entity type — frozen by
|
|
2425
|
+
// test/chat.test.mjs's "no symbol matching \"a.mjs\"" case); only the
|
|
2426
|
+
// remaining catch-all default (unconditionally "module") is replaced by the
|
|
2427
|
+
// parsed AST's own entityType when one is present (Tier-2 playtest, cycle 8
|
|
2428
|
+
// — "which classes inherit from Widget" used to say "no MODULE matching
|
|
2429
|
+
// 'Widget'" even though entityType="Class" was sitting right there unused,
|
|
2430
|
+
// and "Widget" is neither sha- nor dotted-symbol-shaped so the catch-all is
|
|
2431
|
+
// exactly what fired).
|
|
2412
2432
|
const objText = String(parsed.object || "").trim();
|
|
2433
|
+
const fallback = parsed.entityType && PLURAL_FORMS[parsed.entityType] ? nounFor(parsed.entityType, 1) : "module";
|
|
2413
2434
|
const what = /^(?:commit[:\s])?[0-9a-f]{7,40}$/i.test(objText) ? "commit"
|
|
2414
|
-
: (!objText.includes("/") && /^[\w$]+(\.[\w$]+)+$/.test(objText) ? "symbol" :
|
|
2435
|
+
: (!objText.includes("/") && /^[\w$]+(\.[\w$]+)+$/.test(objText) ? "symbol" : fallback);
|
|
2415
2436
|
return {
|
|
2416
2437
|
content: `no ${what} matching "${parsed.object}" found in the index.`,
|
|
2417
2438
|
miss: true, ambiguous: false, candidates: [],
|
package/src/chat.mjs
CHANGED
|
@@ -1426,6 +1426,31 @@ function teachSuggestion(payload) {
|
|
|
1426
1426
|
return `every ${subject} is ${article} ${object}`;
|
|
1427
1427
|
}
|
|
1428
1428
|
|
|
1429
|
+
/** PRONOUN-SUBJECT GUARD (2026-07-08, operator repro): "remember you are a
|
|
1430
|
+
* womble" and the literal "every you is a womble" both used to reach
|
|
1431
|
+
* teachSuggestion/unknownSubjectFallback treating "you" like an ordinary
|
|
1432
|
+
* unknown common noun — producing the nonsensical "did you mean: every you
|
|
1433
|
+
* is a womble" hint (teachSuggestion), or, worse, a SILENT direct-write via
|
|
1434
|
+
* unknownSubjectFallback whenever the object happened to resolve as a known
|
|
1435
|
+
* noun/adjective (e.g. "he is a doctor" would have stored the bogus fact
|
|
1436
|
+
* "he rdfs:subClassOf doctor"). A personal pronoun is never a valid class-
|
|
1437
|
+
* membership subject for ANY object — "every <pronoun> is a Y" isn't
|
|
1438
|
+
* coherent English no matter what Y is, so this is a grammatical category
|
|
1439
|
+
* error, not "new vocabulary" the unknown-subject free pass exists for.
|
|
1440
|
+
* Checked FIRST in teachLane, before any other recognizer gets a look at
|
|
1441
|
+
* the payload (bare OR remember-wrapped surface, so it fires uniformly
|
|
1442
|
+
* across entry points), and short-circuits with its own honest, distinct
|
|
1443
|
+
* decline — never the generic "every X is a Y" miss text, and never a "did
|
|
1444
|
+
* you mean" guess.
|
|
1445
|
+
*
|
|
1446
|
+
* Deliberately limited to the seven UNAMBIGUOUS personal pronouns (you/i/
|
|
1447
|
+
* it/they/he/she/we) — this/that/these/those are excluded on purpose: they
|
|
1448
|
+
* double as legitimate demonstrative entity references elsewhere in this
|
|
1449
|
+
* file (DESCRIBE_PRONOUN_RE, NEGATION_PRONOUN_RE et al.), and a claim about
|
|
1450
|
+
* a demonstrated entity ("that is a bug", pointing at something real) is a
|
|
1451
|
+
* much closer call than "every you is a womble" — not this bug's territory. */
|
|
1452
|
+
const TEACH_PRONOUN_RE = /^(?:every\s+|each\s+|all\s+|some\s+|a few\s+|a\s+|an\s+)?(you|i|it|they|he|she|we)\s+(?:is|are|am)\b/i;
|
|
1453
|
+
|
|
1429
1454
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
1430
1455
|
const rawInput = String(query).trim();
|
|
1431
1456
|
const m = rawInput.match(TEACH_RE);
|
|
@@ -1441,6 +1466,24 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
1441
1466
|
const raw = stripYour(rawInput);
|
|
1442
1467
|
const wrapped = stripYour(wrappedInput);
|
|
1443
1468
|
|
|
1469
|
+
// PRONOUN-SUBJECT GUARD — tried against BOTH surfaces (bare and remember-
|
|
1470
|
+
// wrapped; trailing punctuation stripped the same way the OWNS/SOME_A_FEW
|
|
1471
|
+
// lanes below do) before anything else in this function, so a pronoun
|
|
1472
|
+
// subject NEVER reaches teachSuggestion's "did you mean" hint or
|
|
1473
|
+
// unknownSubjectFallback's direct-write path — see TEACH_PRONOUN_RE's own
|
|
1474
|
+
// docblock above for why.
|
|
1475
|
+
const pronounSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
1476
|
+
const pronounMatch = pronounSrc.match(TEACH_PRONOUN_RE);
|
|
1477
|
+
if (pronounMatch) {
|
|
1478
|
+
const pronoun = pronounMatch[1];
|
|
1479
|
+
return {
|
|
1480
|
+
text: `I can't store a fact about "${pronoun}" as a class — pronouns aren't things I can classify. `
|
|
1481
|
+
+ `I remember facts in the shape "every X is a Y", where X is a specific noun, not a pronoun. `
|
|
1482
|
+
+ "Type /memory to see what I already remember.",
|
|
1483
|
+
via: "teach-miss", miss: true,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1444
1487
|
// OWNERSHIP — "<Name> owns/maintains <X>", bare or remember-wrapped. The bare
|
|
1445
1488
|
// form is double-gated: a Capitalized name AND no interrogative lead, so the
|
|
1446
1489
|
// "who owns <X>" READ question and ordinary prose never land a fact here.
|
|
@@ -1766,6 +1809,24 @@ const PERSONAL_ASSISTANT_NUDGE_RE = new RegExp(
|
|
|
1766
1809
|
const STACCATO_NEGATION_RE = /^(?:and\s+)?(?:not|except(?:\s+for)?)\s+(.+?)(?:\s+then|\s+though)?[?.!]*$/i;
|
|
1767
1810
|
const NEGATION_PRONOUN_RE = /^(?:it|that|this|those|them)(?:\s+ones?)?$/i;
|
|
1768
1811
|
|
|
1812
|
+
/** STACCATO COMPARATIVE (SKILL_CHAT_PLAYTEST Tier-2, 6th pass, cycle 8 —
|
|
1813
|
+
* cycle 7's own recommendation): "more than that", "which is bigger", "is
|
|
1814
|
+
* there anything bigger", "bigger than that" following a superlative answer
|
|
1815
|
+
* ("which module has the most imports" -> "src/handlers/tasks.mjs — 5").
|
|
1816
|
+
* Genuinely unanswerable as a real graph query, never fabricated: tmct's
|
|
1817
|
+
* superlative only ever names the single top (or bottom) match for a metric
|
|
1818
|
+
* (evalSuperlative) — it has no "runner-up"/"next ranked" or "greater than a
|
|
1819
|
+
* number" capability to reach for. Before this, both a short/non-codeish
|
|
1820
|
+
* phrasing ("more than that") and the wall these route to (once the
|
|
1821
|
+
* isConversational catch-all is deferred below) fell to the generic
|
|
1822
|
+
* orientation card or the raw grammar wall — neither says what actually went
|
|
1823
|
+
* wrong. Same "honest, guiding nudge, never a bare wall" discipline as
|
|
1824
|
+
* STACCATO_NEGATION_RE just above; the standing focus (now the superlative
|
|
1825
|
+
* WINNER after the chat.mjs fix that made a superlative set it) names what
|
|
1826
|
+
* the user can compare against directly. */
|
|
1827
|
+
const STACCATO_COMPARATIVE_RE =
|
|
1828
|
+
/^(?:(?:is\s+there\s+|what(?:'s|\s+is)\s+)?(?:anything|something)?\s*(?:more|bigger|larger|smaller|fewer|less)\s*(?:than\s+(?:that|it|this))?|which(?:\s+one)?\s+is\s+(?:bigger|smaller|larger|more|less))\s*\??$/i;
|
|
1829
|
+
|
|
1769
1830
|
/** The <name> a nudge shows: the focus label when the query leans on a pronoun (or
|
|
1770
1831
|
* gave us nothing better), else the captured subject; "<name>" as the placeholder. */
|
|
1771
1832
|
function nudgeName(captured, focus) {
|
|
@@ -1815,6 +1876,31 @@ function nudgeAnswer(query, focus) {
|
|
|
1815
1876
|
return "I can't filter a previous list by exclusion yet — ask the positive shape directly "
|
|
1816
1877
|
+ `(e.g. "which modules import <name>"), or ask about ${term} on its own.`;
|
|
1817
1878
|
}
|
|
1879
|
+
if (STACCATO_COMPARATIVE_RE.test(q)) {
|
|
1880
|
+
const name = focus?.label;
|
|
1881
|
+
return "I only name the single top (or bottom) match for a metric — no runner-up ranking, no comparing against a number. "
|
|
1882
|
+
+ (name
|
|
1883
|
+
? `Ask about a specific module/class/function directly to compare it with ${name} (e.g. "how many imports does <name> have").`
|
|
1884
|
+
: `Ask a specific ranking directly, e.g. "which module has the most imports".`);
|
|
1885
|
+
}
|
|
1886
|
+
// A bare STACCATO PRONOUN continuation ("also that one?", "and it") with NO
|
|
1887
|
+
// standing focus at all (Tier-2 playtest, 6th pass, cycle 8): describeWrapperAnswer
|
|
1888
|
+
// (4d, below) already resolves this shape perfectly when a real focus stands
|
|
1889
|
+
// (T18) — it honestly DECLINES (null) when there is none, same discipline as
|
|
1890
|
+
// every other focus-dependent lane. Before this branch, that decline fell
|
|
1891
|
+
// through all the way to the generic multi-line orientation card, which names
|
|
1892
|
+
// nothing about what actually went wrong. Symmetric with STACCATO_NEGATION_RE's
|
|
1893
|
+
// own no-focus nudge just above ("not sure what you'd like instead… — name it
|
|
1894
|
+
// directly"): a positive pronoun with nothing to point at gets the same honest,
|
|
1895
|
+
// tailored decline instead of the wall. (Exposed by the STACCATO_LEAKED_CONNECTIVES
|
|
1896
|
+
// fix in runAsk: "what about imports" -> "and calls?" no longer silently — and
|
|
1897
|
+
// WRONGLY — installs a substring-matched module as focus, so a chain of two
|
|
1898
|
+
// vague relation touches genuinely has no antecedent for a third "also that
|
|
1899
|
+
// one?" to resolve — this nudge is what such a chain should always have gotten.)
|
|
1900
|
+
const pronounContinuation = q.match(STACCATO_PRONOUN_RE);
|
|
1901
|
+
if (pronounContinuation && !focus?.label) {
|
|
1902
|
+
return `not sure what "${pronounContinuation[1].toLowerCase()}" refers to yet — name something directly, e.g. "what calls <name>".`;
|
|
1903
|
+
}
|
|
1818
1904
|
return null;
|
|
1819
1905
|
}
|
|
1820
1906
|
|
|
@@ -2830,6 +2916,22 @@ const NAME_TOKEN_RE = /\b[\w-]+(?:[/.][\w-]+)+\b|\b[A-Z][A-Za-z0-9_]*\b|\b[a-z][
|
|
|
2830
2916
|
* matches and falls through unchanged. */
|
|
2831
2917
|
const STACCATO_SWAP_RE = /^(?:and|also|so|then|now)\s+(.+?)[?.!\s]*$/i;
|
|
2832
2918
|
|
|
2919
|
+
/** The five bare connective words STACCATO_SWAP_RE/relationTermOf's own STACCATO
|
|
2920
|
+
* branch lead with. Reused (runAsk's focus-resolution guard, below) to catch
|
|
2921
|
+
* the case where ask()'s OWN raw grammar, given an unstripped "and calls?",
|
|
2922
|
+
* happens to recognize "calls" as a verb and leaves the leading "and" as
|
|
2923
|
+
* parsed.object — a leaked connective, never real content, must never be fed
|
|
2924
|
+
* to resolveEntity (see that guard's own docblock for the concrete failure). */
|
|
2925
|
+
const STACCATO_LEAKED_CONNECTIVES = new Set(["and", "also", "so", "then", "now"]);
|
|
2926
|
+
|
|
2927
|
+
/** A whole-word occurrence of one of chat.mjs's own closed antecedent pronouns
|
|
2928
|
+
* (CONTEXT_WORDS: "it"/"this"/"that"/"here") inside a PRIOR turn's raw query
|
|
2929
|
+
* text — discourseRewrite's fallback swap target when that text has no real
|
|
2930
|
+
* NAME_TOKEN at all ("what calls it", "where is it defined"). Word-boundary
|
|
2931
|
+
* anchored so a real identifier merely CONTAINING one of these (e.g. a symbol
|
|
2932
|
+
* named `edithistory`) is never mistaken for the pronoun. */
|
|
2933
|
+
const PRONOUN_IN_QUERY_RE = new RegExp(`\\b(?:${[...CONTEXT_WORDS].join("|")})\\b`, "i");
|
|
2934
|
+
|
|
2833
2935
|
/** DISCOURSE CONTINUATION (CHATBENCH_006 lever 2): "what about X" carries the PRIOR
|
|
2834
2936
|
* turn's question shape across the turn boundary — re-asking it with X in place of
|
|
2835
2937
|
* the previous subject/object. Returns the reconstructed query (parsed like any
|
|
@@ -2848,8 +2950,51 @@ function discourseRewrite(query, last) {
|
|
|
2848
2950
|
}
|
|
2849
2951
|
if (!last?.query) return null;
|
|
2850
2952
|
const prevQ = String(last.query);
|
|
2851
|
-
if (
|
|
2852
|
-
|
|
2953
|
+
if (NAME_TOKEN_RE.test(prevQ)) return prevQ.replace(NAME_TOKEN_RE, () => newSubj);
|
|
2954
|
+
// PRONOUN-ANTECEDENT PRIOR QUERY (Tier-2 playtest, 6th pass, cycle 8, deep
|
|
2955
|
+
// multi-hop relation-touch chain stress-test): the prior turn can ITSELF be
|
|
2956
|
+
// pronoun-shaped ("what calls it", "where is it defined" — a drill-down
|
|
2957
|
+
// step that resolved "it" against the standing focus rather than naming an
|
|
2958
|
+
// entity literally) — such a query has NO NAME_TOKEN at all to swap, so the
|
|
2959
|
+
// rule above always declined and a perfectly natural next hop ("and Task?"
|
|
2960
|
+
// meaning "and what calls Task?") fell straight to the raw grammar wall.
|
|
2961
|
+
// Swap the bare pronoun itself in that case ("what calls it" -> "what calls
|
|
2962
|
+
// Task") — CONTEXT_WORDS is chat.mjs's own closed antecedent-pronoun set
|
|
2963
|
+
// (the same one isPronoun/the focus-reuse guard above already trust), so
|
|
2964
|
+
// this only ever touches a genuine referring pronoun, never a real word
|
|
2965
|
+
// that happens to contain "it"/"this"/"that" as a substring (whole-word
|
|
2966
|
+
// boundaries only).
|
|
2967
|
+
if (PRONOUN_IN_QUERY_RE.test(prevQ)) return prevQ.replace(PRONOUN_IN_QUERY_RE, () => newSubj);
|
|
2968
|
+
return null;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
/** STACCATO SUPERLATIVE REPEAT (Tier-2 playtest, 6th pass, cycle 8): "the biggest
|
|
2972
|
+
* one" / "which is biggest" / "which one is the biggest" / "what about the
|
|
2973
|
+
* biggest one" continuing a superlative last turn ("which module has the most
|
|
2974
|
+
* imports") names NO entity kind at all — parseSuperlative's own grammar always
|
|
2975
|
+
* declines that shape ("a superlative needs an entity kind (module, class,
|
|
2976
|
+
* function, …)"), the one piece of information every OTHER superlative phrasing
|
|
2977
|
+
* supplies, and unlike a plain object ("what does it import") there is no
|
|
2978
|
+
* pronoun slot here for the focus to fill. Rather than guess a NEW metric — a
|
|
2979
|
+
* bare "biggest" with an entity kind spliced in would default to the generic
|
|
2980
|
+
* "connections" metric (EDGE_NOUN_TO_METRIC.connections), which is NOT what
|
|
2981
|
+
* "the biggest one" means right after a query about imports specifically, and
|
|
2982
|
+
* would silently answer a different question than the one just asked — this
|
|
2983
|
+
* re-asks the PRIOR superlative query VERBATIM: the user is confirming/
|
|
2984
|
+
* repeating the same ranking in their own words, not asking a new one, so
|
|
2985
|
+
* replaying the exact prior text (same entityType, same metric) is the only
|
|
2986
|
+
* non-fabricating reading. Gated on the prior query textually naming an extreme
|
|
2987
|
+
* word — a bare "the biggest one" after an unrelated last turn declines (null)
|
|
2988
|
+
* and the ordinary honest miss stands, same discipline as discourseRewrite's
|
|
2989
|
+
* own NAME_TOKEN_RE gate just above. */
|
|
2990
|
+
const STACCATO_SUPERLATIVE_RE =
|
|
2991
|
+
/^(?:what about\s+)?(?:(?:and|also|so|then|now)\s+)?(?:which(?:\s+one)?\s+is\s+(?:the\s+)?|the\s+)(?:most|greatest|highest|biggest|largest|fewest|least|smallest)(?:[- ]connected)?(?:\s+ones?)?\s*\??$/i;
|
|
2992
|
+
const SUPERLATIVE_EXTREME_WORD_RE = /\b(?:most|greatest|highest|biggest|largest|fewest|least|smallest)\b/i;
|
|
2993
|
+
function superlativeRepeatRewrite(query, last) {
|
|
2994
|
+
if (!STACCATO_SUPERLATIVE_RE.test(String(query).trim())) return null;
|
|
2995
|
+
const prevQ = String(last?.query || "");
|
|
2996
|
+
if (!prevQ || !SUPERLATIVE_EXTREME_WORD_RE.test(prevQ)) return null;
|
|
2997
|
+
return prevQ;
|
|
2853
2998
|
}
|
|
2854
2999
|
|
|
2855
3000
|
// ---- curated SEON definitions (corpus/seon/definitions.jsonl) ----
|
|
@@ -3252,7 +3397,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3252
3397
|
// The query the ENGINE parses: a "what about X" continuation is rewritten to the
|
|
3253
3398
|
// prior shape with X swapped in; everything else parses verbatim. The record and
|
|
3254
3399
|
// transcript keep the user's ACTUAL words (`query`), only the parse target changes.
|
|
3255
|
-
let askQuery = discourseRewrite(query, last) ?? query;
|
|
3400
|
+
let askQuery = superlativeRepeatRewrite(query, last) ?? discourseRewrite(query, last) ?? query;
|
|
3256
3401
|
// IMPLICIT ANAPHORIC COUNT (Tier-2 playtest, 5th pass): "how many are tested" /
|
|
3257
3402
|
// "and how many are tested" drops the "of those/them" a fuller phrasing carries
|
|
3258
3403
|
// — ask()'s own anaphora node (parseAnaphora) already understands "how many of
|
|
@@ -3336,14 +3481,47 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3336
3481
|
// it becomes the new focus so a follow-up "what calls it" can reuse it.
|
|
3337
3482
|
let resolvedIds = [];
|
|
3338
3483
|
let newFocus = focus;
|
|
3339
|
-
|
|
3484
|
+
// STACCATO CONNECTIVE LEAKAGE (Tier-2 playtest, 6th pass, cycle 8, multi-hop
|
|
3485
|
+
// relation-touch chain stress-test): "and calls?" — the bare-connective
|
|
3486
|
+
// relation-chain continuation STACCATO_SWAP_RE/relationTermOf's own STACCATO
|
|
3487
|
+
// branch both recognize — is handed to ask() UNSTRIPPED as askQuery. When
|
|
3488
|
+
// ask()'s own raw grammar happens to recognize "calls" as a verb (VERB_TO_KIND),
|
|
3489
|
+
// the leftover "and" becomes parsed.object, and resolveObject's tier-3
|
|
3490
|
+
// substring match (`label.includes(tLc)`) has no minimum-length floor — a
|
|
3491
|
+
// 2-3 letter connective is a near-certain accidental substring of SOME real
|
|
3492
|
+
// label ("and" -> Controller.h-AND-le). The visible answer still looks fine
|
|
3493
|
+
// (relationForceAnswer, later, composes the correct generic relation text
|
|
3494
|
+
// over the SAME query) — but this block ran FIRST and silently rebound the
|
|
3495
|
+
// FOCUS to that bogus match, so the NEXT turn's pronoun ("what tests it")
|
|
3496
|
+
// resolved against the wrong entity and rendered a confidently WRONG empty
|
|
3497
|
+
// ("no tests cover it") for a module that genuinely has tests. The exact
|
|
3498
|
+
// same class of bug as CHATBENCH_0.7.1's "it" reuse-focus fix just below —
|
|
3499
|
+
// grammar scaffolding leaked into the object slot is never real content, so
|
|
3500
|
+
// (mirroring that fix's own discipline) any of the five closed connective
|
|
3501
|
+
// words STACCATO_SWAP_RE recognizes is excluded here from ever being resolved
|
|
3502
|
+
// as an object at all: the branch is skipped entirely, leaving the standing
|
|
3503
|
+
// focus untouched for the relation force (or ordinary miss) to answer over.
|
|
3504
|
+
const isLeakedConnective = STACCATO_LEAKED_CONNECTIVES.has(String(envelope?.parsed?.object || "").toLowerCase());
|
|
3505
|
+
if (graph && envelope?.parsed?.object && !isLeakedConnective) {
|
|
3340
3506
|
const obj = envelope.parsed.object;
|
|
3341
3507
|
// A PRONOUN object ("it"/"this") was already resolved against the focus via
|
|
3342
3508
|
// contextId — the resolved antecedent IS the focus. Re-resolving the literal
|
|
3343
3509
|
// pronoun string is the CHATBENCH_0.7.1 B1-pron bug: "it" substring-matches the
|
|
3344
3510
|
// "Commit" schema node (label contains "it"), so the focus jumped off the module
|
|
3345
|
-
// to a Commit and the NEXT "it" bound wrong. Reuse the focus directly instead
|
|
3346
|
-
|
|
3511
|
+
// to a Commit and the NEXT "it" bound wrong. Reuse the focus directly instead —
|
|
3512
|
+
// and when there is NO focus to reuse (Tier-2 playtest, 6th pass, cycle 8: a
|
|
3513
|
+
// bare "where is it defined"/"what does it import" with nothing standing yet,
|
|
3514
|
+
// or right after a superlative TIE, which deliberately never sets one — see
|
|
3515
|
+
// the superlative-winner branch below), never fall through to resolveEntity on
|
|
3516
|
+
// the raw pronoun string either: that is the EXACT SAME substring-match trap
|
|
3517
|
+
// (a 2-letter "it" is a near-certain accidental substring of SOME real label —
|
|
3518
|
+
// here, Task.t-IT-le) as STACCATO_LEAKED_CONNECTIVES fixes for "and"/"also"
|
|
3519
|
+
// above, just triggered by a pronoun instead of a connective. ask()'s OWN
|
|
3520
|
+
// evaluation already renders the honest "'it' needs a selected node…" miss in
|
|
3521
|
+
// this case (contextId was null); silently adopting a bogus focus as a side
|
|
3522
|
+
// effect here would corrupt the NEXT turn's pronoun into a confidently WRONG
|
|
3523
|
+
// (not just empty) answer, exactly as the connective leak did.
|
|
3524
|
+
const ent = isPronoun(obj) ? (focus?.id ? focus : null) : await resolveEntity(graph, obj);
|
|
3347
3525
|
if (ent) {
|
|
3348
3526
|
resolvedIds = [ent.id];
|
|
3349
3527
|
// Class-gate the focus update: a Commit/Session/schema object never displaces a
|
|
@@ -3353,6 +3531,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3353
3531
|
} else if (!isPronoun(obj)) {
|
|
3354
3532
|
note(trace, `intermediate: object "${obj}" did NOT resolve to a graph entity — this is why an otherwise-parsed query still misses`);
|
|
3355
3533
|
}
|
|
3534
|
+
} else if (graph && envelope?.parsed?.node === "superlative" && Array.isArray(envelope?.matches) && envelope.matches.length === 1) {
|
|
3535
|
+
// A superlative ("which module has the most imports") names no object at all —
|
|
3536
|
+
// the branch above never runs — so the ranked WINNER never became the focus,
|
|
3537
|
+
// and an immediate natural follow-up ("what does it import", "where is it
|
|
3538
|
+
// defined") dead-ended on "'it' needs a selected node to refer to" right after
|
|
3539
|
+
// the engine had just named one. Mirrors the object-resolution rule above
|
|
3540
|
+
// exactly: a single, unambiguous winner (no tie — a multi-way tie names no
|
|
3541
|
+
// one individual, so the focus is left alone rather than guessing which of
|
|
3542
|
+
// the tied matches the user means) becomes the new focus, class-gated the
|
|
3543
|
+
// same way (nextFocus). Found in Tier 2 playtest, cycle 8 (superlative
|
|
3544
|
+
// follow-up chains, SKILL_CHAT_PLAYTEST.md).
|
|
3545
|
+
const winner = envelope.matches[0];
|
|
3546
|
+
if (winner?.id) {
|
|
3547
|
+
resolvedIds = [winner.id];
|
|
3548
|
+
newFocus = nextFocus(graph, focus, winner);
|
|
3549
|
+
note(trace, `result: superlative winner ${winner.label} (${winner.id}) — becomes the new focus`);
|
|
3550
|
+
}
|
|
3356
3551
|
}
|
|
3357
3552
|
const answeredIds = (envelope?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
3358
3553
|
const miss = envelope ? !!envelope.miss : true;
|
|
@@ -3441,7 +3636,50 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
3441
3636
|
// branch ALWAYS returns a tailored nudge for this shape, never null, so
|
|
3442
3637
|
// deferring here never strands the turn with nothing having claimed it.
|
|
3443
3638
|
const isStaccatoNegation = STACCATO_NEGATION_RE.test(String(query).trim());
|
|
3444
|
-
|
|
3639
|
+
// Staccato comparative ("more than that", "which is bigger", "is there
|
|
3640
|
+
// anything bigger" — Tier-2 playtest, 6th pass, cycle 8) needs the SAME
|
|
3641
|
+
// deferral, for the SAME reason as isStaccatoNegation just above: these are
|
|
3642
|
+
// short/non-codeish and trip isConversational's ≤3-word catch-all before
|
|
3643
|
+
// nudgeAnswer's own STACCATO_COMPARATIVE_RE branch ever gets a turn.
|
|
3644
|
+
// nudgeAnswer's comparative branch ALWAYS returns a tailored nudge for this
|
|
3645
|
+
// shape, never null, so deferring here never strands the turn unclaimed.
|
|
3646
|
+
const isStaccatoComparative = STACCATO_COMPARATIVE_RE.test(String(query).trim());
|
|
3647
|
+
// A bare STACCATO PRONOUN continuation ("also that one?", "and it") with NO
|
|
3648
|
+
// standing focus (Tier-2 playtest, 6th pass, cycle 8) needs the SAME deferral:
|
|
3649
|
+
// nudgeAnswer's own STACCATO_PRONOUN_RE-no-focus branch (just above) ALWAYS
|
|
3650
|
+
// returns a tailored nudge for this exact shape, never null, so deferring
|
|
3651
|
+
// here never strands the turn unclaimed — see that branch's own docblock for
|
|
3652
|
+
// why the no-focus case must never reach the generic orientation card.
|
|
3653
|
+
const isStaccatoPronounNoFocus = STACCATO_PRONOUN_RE.test(String(query).trim()) && !focus?.label;
|
|
3654
|
+
// A vague relation touch ("what about cochange", "tell me about cochange",
|
|
3655
|
+
// the staccato chain continuation "and cochange?") whose relation word has NO
|
|
3656
|
+
// bare single-word VERB_TO_KIND form of its own needs the SAME deferral as
|
|
3657
|
+
// isExplainTouch just above, for the identical reason: "cochange" is the one
|
|
3658
|
+
// relation (ask-vocab.mjs RELATIONS.cochange) whose every registered verb
|
|
3659
|
+
// phrase takes a preposition ("changed WITH X", "changes together WITH X") —
|
|
3660
|
+
// there is no bare "cochanges X" — so ask() never gives this shape a parse to
|
|
3661
|
+
// hang envelope.parsed off of, unlike its siblings (imports/calls/tests/
|
|
3662
|
+
// inherits/contains/defines/touches/reexports all have a bare verb and so
|
|
3663
|
+
// already escape isConversational's ≤3-word catch-all via envelope.parsed).
|
|
3664
|
+
// 0.9.16 Tier-2 playtest, 6th pass: "what about cochange" as an opening turn,
|
|
3665
|
+
// and "and cochange?" as a mid-chain continuation after a working "what about
|
|
3666
|
+
// tests", both fell straight to the generic orientation card even though the
|
|
3667
|
+
// graph has real cochange edges and every sibling relation word already
|
|
3668
|
+
// flowed. Needs no prior-turn/focus context either (same as isExplainTouch) —
|
|
3669
|
+
// scoped to the CLOSED RELATION_TERM vocabulary (concept.mjs) via
|
|
3670
|
+
// relationTermOf's own gate, so an unknown word or a real entity name still
|
|
3671
|
+
// declines and isConversational's catch-all is untouched for it.
|
|
3672
|
+
let isVagueRelationTouch = false;
|
|
3673
|
+
{
|
|
3674
|
+
const relTerm = relationTermOf(String(query), envelope);
|
|
3675
|
+
if (relTerm) {
|
|
3676
|
+
try {
|
|
3677
|
+
const { RELATION_TERM } = await import("./concept.mjs");
|
|
3678
|
+
isVagueRelationTouch = !!RELATION_TERM[relTerm.toLowerCase()];
|
|
3679
|
+
} catch { /* leave false — the ordinary path decides */ }
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
if (!handled && miss && !envelope?.parsed && isConversational(query) && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus) {
|
|
3445
3683
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
3446
3684
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
3447
3685
|
// Bug B1 (0.8.2 follow-up): this branch carries via:"template" and never
|