@polycode-projects/the-mechanical-code-talker 1.8.16 → 1.8.17
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-vocab.mjs +9 -0
- package/src/ask.mjs +17 -1
- package/src/chat.mjs +239 -15
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.17",
|
|
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-vocab.mjs
CHANGED
|
@@ -644,6 +644,15 @@ export const FILLER_WORDS = Object.freeze([
|
|
|
644
644
|
"please", "could you", "can you", "would you", "tell me", "i wonder",
|
|
645
645
|
"just wondering", "quickly", "real quick", "kinda", "sorta",
|
|
646
646
|
"btw", "by the way", "you",
|
|
647
|
+
// "quick q" (BENCHMARK_CONVERSATION_1.8.14.md item 11): the casual abbreviated
|
|
648
|
+
// sibling of GREETING_PREAMBLE_RE's own "quick question" clause (normalize.mjs)
|
|
649
|
+
// — that frame requires a delimiter immediately after the greeting word
|
|
650
|
+
// ("hey, quick question - …"), so it never matches "hey quick q, …" (no
|
|
651
|
+
// delimiter between "hey" and "quick q"). Filler-stripping instead — this
|
|
652
|
+
// list is matched word-boundary-anywhere, not anchored — closes the gap
|
|
653
|
+
// without needing GREETING_PREAMBLE_RE's own stricter delimiter-position
|
|
654
|
+
// shape.
|
|
655
|
+
"quick q",
|
|
647
656
|
]);
|
|
648
657
|
|
|
649
658
|
/** Deictic/pronoun terms that refer to a context entity rather than naming one
|
package/src/ask.mjs
CHANGED
|
@@ -3196,7 +3196,23 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
3196
3196
|
if (shape === "ask") {
|
|
3197
3197
|
const subj = resolveTermOrContext(graph, parsed.subject, contextId);
|
|
3198
3198
|
const obj = resolveTermOrContext(graph, parsed.object, contextId);
|
|
3199
|
-
|
|
3199
|
+
// BENCHMARK_CONVERSATION_1.8.14.md item 7(b): a term that resolved only via a
|
|
3200
|
+
// TIED, ambiguous fuzzy/prose match (e.g. a garbled object phrase like "handler
|
|
3201
|
+
// I think" — trailing hedge noise a plain declarative leaked into the object
|
|
3202
|
+
// slot) used to fall straight through as if `obj`/`subj` were a confident
|
|
3203
|
+
// single match, so a randomly-first-picked TIED candidate (once, live, a raw
|
|
3204
|
+
// Commit whose "label" IS its short hash, "c3d4e5f6a1b2") rode straight into
|
|
3205
|
+
// the Yes/No render's "No — no <kind> edge found from A to B" sentence — an
|
|
3206
|
+
// internal id leaking into user-facing text off the back of an unresolved tie,
|
|
3207
|
+
// not a real resolution. Every OTHER shape in this file already declines
|
|
3208
|
+
// rather than guessing on a tie (resolveObject's own "never a guess" contract,
|
|
3209
|
+
// its docblock above); this shape is the one place that never checked the
|
|
3210
|
+
// `ambiguous` flag its own resolver already computed. Declining here (leaving
|
|
3211
|
+
// subjMatch unset, same as the `!subj.match || !obj.match` miss just below)
|
|
3212
|
+
// reaches the SAME "couldn't resolve one of the terms in this question" honest
|
|
3213
|
+
// miss render() already uses — never a confident wrong answer, and never a
|
|
3214
|
+
// raw internal id surfacing off a coin-flip pick among tied candidates.
|
|
3215
|
+
if (!subj.match || !obj.match || subj.ambiguous || obj.ambiguous) {
|
|
3200
3216
|
return {
|
|
3201
3217
|
matches: [], objMatch: obj.match, candidates: obj.candidates, traversal: null, ambiguous: false, answer: null,
|
|
3202
3218
|
unresolvedPronoun: !!(subj.unresolvedPronoun || obj.unresolvedPronoun),
|
package/src/chat.mjs
CHANGED
|
@@ -813,6 +813,13 @@ const CAPABILITY_PHRASES = [
|
|
|
813
813
|
// and fall to the raw grammar wall instead of orientationAnswer — the same
|
|
814
814
|
// question in every way that matters, just phrased with emphasis.
|
|
815
815
|
/^(?:so,?\s+)?what can (?:you|u)(?:\s+(?:actually|really))? do\??$/i, /^(?:so,?\s+)?what do you(?:\s+(?:actually|really))? do\??$/i,
|
|
816
|
+
// BENCHMARK_CONVERSATION_1.8.14.md item 12 (pure-small-talk persona): "what
|
|
817
|
+
// can you actually help with" — the natural pivot from small talk into a
|
|
818
|
+
// capability question — used to miss every entry above, since none of them
|
|
819
|
+
// accept "help (me)? with" as a synonym tail for "do" (only bare "help" /
|
|
820
|
+
// "help me" without an object, the entry just below, was covered). Same
|
|
821
|
+
// "so,"/"actually"/"really" optional lead-in as the "do" pair above.
|
|
822
|
+
/^(?:so,?\s+)?what can (?:you|u)(?:\s+(?:actually|really))? help(?:\s+me)?\s+with\??$/i,
|
|
816
823
|
/^help( me)?\??$/i, /^\?+$/,
|
|
817
824
|
/^how do (i|you) work\??$/i, /^how does (this|it) work\??$/i,
|
|
818
825
|
// unix-habit openers typed inside the REPL out of muscle memory — argv-only
|
|
@@ -938,6 +945,20 @@ const FEELINGS_PHRASES = [
|
|
|
938
945
|
/^are you (?:sentient|conscious|self[- ]aware)\??$/i,
|
|
939
946
|
/^can you feel(?:\s+(?:things|emotions|anything))?\??$/i,
|
|
940
947
|
/^do you (?:feel|think|dream)\??$/i,
|
|
948
|
+
// BENCHMARK_CONVERSATION_1.8.14.md item 12 (pure-small-talk persona): direct
|
|
949
|
+
// personal questions ("how are you doing today", "what's your favorite
|
|
950
|
+
// color", "do you get bored") — the SAME family this closed set already
|
|
951
|
+
// exists for (a personal-life question about tmct, not a code-graph query)
|
|
952
|
+
// — mostly fell through to the bare grammar wall instead of this honest,
|
|
953
|
+
// on-brand decline, because none of them happened to match the four
|
|
954
|
+
// narrower entries above. Additive, same discipline as CAPABILITY_PHRASES'
|
|
955
|
+
// own "keeps growing as new natural phrasings surface" precedent.
|
|
956
|
+
/^how (?:are|r) (?:you|u) doing(?:\s+today)?\??$/i,
|
|
957
|
+
/^what(?:'s|s|\s+is) your (?:favou?rite\s+(?:colou?r|food|movie|book|band|song|number)|name)\??$/i,
|
|
958
|
+
/^do you (?:get|ever get) bored\??$/i,
|
|
959
|
+
/^what do you do for fun\??$/i,
|
|
960
|
+
/^can you (?:tell|make)\s+(?:me\s+)?(?:a\s+)?jokes?\??$/i,
|
|
961
|
+
/^do you (?:know|know anything|know much)\s+about\s+(?:movies?|sports?|music|tv|television)(?:\s+or\s+(?:movies?|sports?|music|tv|television))?\??$/i,
|
|
941
962
|
];
|
|
942
963
|
/** The structural verbs/nouns that mark a near-miss code question (→ keep the
|
|
943
964
|
* precise grammar hint, not the friendly nudge). */
|
|
@@ -1687,6 +1708,14 @@ const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+ (?:is|are) (?:
|
|
|
1687
1708
|
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
1688
1709
|
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
1689
1710
|
const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
1711
|
+
/** A plain declarative "X is a kind of Y" / "X is a Y" shape (subject-first, no
|
|
1712
|
+
* question lead — paired with QUESTION_LEAD_RE at every call site), tolerating
|
|
1713
|
+
* an infix "kind of"/"type of" (teachLane's own stripKindOf handles this same
|
|
1714
|
+
* infix ahead of its narrower recognizers — this is a cheap TRIGGER check only,
|
|
1715
|
+
* not a storage decision). Used by runAsk's relaxedTeachCollision guard (below)
|
|
1716
|
+
* to recognize when a query the ask engine "answered" via relaxation was
|
|
1717
|
+
* actually a teach-shaped sentence, not a real question. */
|
|
1718
|
+
const DECLARATIVE_KIND_OF_RE = /^(?:every\s+|each\s+|all\s+|a\s+|an\s+)?[\w-]+(?:\s+[\w-]+)?\s+(?:is|are)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?[\w-]+[.!]*$/i;
|
|
1690
1719
|
/** A bare wh-word token, tested one word at a time against `hasMidSentenceInterrogative`'s
|
|
1691
1720
|
* own tokenization below — never re-anchored, so it matches at ANY word position. */
|
|
1692
1721
|
const MID_SENTENCE_WH_RE = /^(?:which|who|what|where|when|why|how)$/i;
|
|
@@ -2698,6 +2727,24 @@ function teachSuggestion(payload) {
|
|
|
2698
2727
|
* way it already stood ahead of teachSuggestion/unknownSubjectFallback. */
|
|
2699
2728
|
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+\S+/i;
|
|
2700
2729
|
|
|
2730
|
+
/** RETRACTION / NEGATION of an already-taught subClassOf fact (PLAN_SYLLOGIST.md
|
|
2731
|
+
* §3 finding 1 fix): "X is not a Y" (tolerating the same "kind/type of" infix
|
|
2732
|
+
* every other teach shape in this lane already tolerates — the "is/are"
|
|
2733
|
+
* variant, plus the "isn't"/"aren't" contractions). Deliberately narrow — the
|
|
2734
|
+
* SAFEST, most unambiguous negation phrasing only, matching a scoped 2-token
|
|
2735
|
+
* subject (mirrors UNKNOWN_SUBJECT_RE's own subject width), never a general
|
|
2736
|
+
* negation grammar. See the RETRACTION block's own comment in teachLane
|
|
2737
|
+
* (below) for why a regex match here is only a TRIGGER, never itself proof a
|
|
2738
|
+
* fact existed to retract — retractSubClassOf (src/syllogise.mjs) is the
|
|
2739
|
+
* actual authority. */
|
|
2740
|
+
const RETRACT_NOT_A_RE = /^(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:(?:is|are)\s+not|isn't|aren't)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?([\w-]+)$/i;
|
|
2741
|
+
/** "forget (that) X is a Y" — the second closed retraction phrasing (PLAN_SYLLOGIST.md
|
|
2742
|
+
* §3 finding 1). Never wrapped by TEACH_RE ("forget" isn't one of its
|
|
2743
|
+
* recognized lead verbs — remember/note/keep in mind/…), so this is matched
|
|
2744
|
+
* against the RAW (unwrapped) sentence, unlike RETRACT_NOT_A_RE above which
|
|
2745
|
+
* is tried against the remember-wrapped surface too. */
|
|
2746
|
+
const RETRACT_FORGET_RE = /^forget\s+(?:that\s+)?(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:is|are)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?([\w-]+)$/i;
|
|
2747
|
+
|
|
2701
2748
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
2702
2749
|
// Tier 6 playtest: this lane read the raw, un-normalized query, so a closed
|
|
2703
2750
|
// discourse-marker preamble ahead of a teach sentence ("howdy pardner,
|
|
@@ -2784,6 +2831,61 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
2784
2831
|
};
|
|
2785
2832
|
}
|
|
2786
2833
|
|
|
2834
|
+
// RETRACTION — "X is not a Y" / "forget that X is a Y" (PLAN_SYLLOGIST.md §3
|
|
2835
|
+
// finding 1 fix, 2026-07-12): the data-layer retraction primitive
|
|
2836
|
+
// (retractSubClassOf, src/syllogise.mjs, PLAN_SYLLOGIST.md §3, commit
|
|
2837
|
+
// f7b3644) shipped with no chat-level phrasing ever calling it — confirmed
|
|
2838
|
+
// live, 5 attempts across 2 playtest personas, 0 successes
|
|
2839
|
+
// (BENCHMARK_CONVERSATION_1.8.14.md). Tried here, right after the pronoun
|
|
2840
|
+
// guard, so a pronoun subject ("it is not an animal") still falls to that
|
|
2841
|
+
// guard's own decline first (TEACH_PRONOUN_RE matches ANY verb after the
|
|
2842
|
+
// pronoun, including "is not"), never reaching this block.
|
|
2843
|
+
//
|
|
2844
|
+
// TRIGGER, never itself the authority: RETRACT_NOT_A_RE/RETRACT_FORGET_RE
|
|
2845
|
+
// only recognize the SHAPE of a negation/retraction sentence — they say
|
|
2846
|
+
// nothing about whether subject⊑object was ever actually taught.
|
|
2847
|
+
// retractSubClassOf is asked for real and is the only thing that decides:
|
|
2848
|
+
// - found:true → a real stored (or entailed) fact existed and was
|
|
2849
|
+
// retracted (with its dependency-directed cascade) — confirmed here.
|
|
2850
|
+
// - found:false → subject⊑object was never a stored fact. This is left
|
|
2851
|
+
// to FALL THROUGH to the rest of teachLane's ordinary cascade below,
|
|
2852
|
+
// deliberately NOT answered with a bespoke "nothing to forget" message
|
|
2853
|
+
// — RETRACT_NOT_A_RE's shape also incidentally matches a NEGATED
|
|
2854
|
+
// PROPERTY claim ("the logger is not deprecated" — never subClassOf-
|
|
2855
|
+
// shaped at all, a pinned "genuine ceiling" case elsewhere in this
|
|
2856
|
+
// codebase, test/chatflow-tier5.test.mjs, that must keep its own
|
|
2857
|
+
// "I couldn't store that —" decline verbatim), so a bare "nothing
|
|
2858
|
+
// found" here must never claim a specific, possibly-wrong reason —
|
|
2859
|
+
// falling through preserves whatever honest response that OTHER shape
|
|
2860
|
+
// already gets, byte-identical, while still fully closing the real gap
|
|
2861
|
+
// (an already-taught fact's retraction, which now always succeeds).
|
|
2862
|
+
const retractSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
2863
|
+
const retractSrcMidQuestion = memoryDir && !QUESTION_LEAD_RE.test(retractSrc)
|
|
2864
|
+
? await hasMidSentenceInterrogative(retractSrc) : false;
|
|
2865
|
+
const retractNotMatch = memoryDir && !QUESTION_LEAD_RE.test(retractSrc) && !retractSrcMidQuestion
|
|
2866
|
+
? retractSrc.match(RETRACT_NOT_A_RE) : null;
|
|
2867
|
+
const forgetSrc = raw.replace(/[.!?]+\s*$/, "");
|
|
2868
|
+
const retractForgetMatch = !retractNotMatch && memoryDir && !QUESTION_LEAD_RE.test(forgetSrc)
|
|
2869
|
+
? forgetSrc.match(RETRACT_FORGET_RE) : null;
|
|
2870
|
+
const retractMatch = retractNotMatch || retractForgetMatch;
|
|
2871
|
+
if (retractMatch) {
|
|
2872
|
+
const retractSubject = retractMatch[1].trim();
|
|
2873
|
+
const retractObject = retractMatch[2].trim();
|
|
2874
|
+
const { retractSubClassOf } = await import("./syllogise.mjs");
|
|
2875
|
+
const result = await retractSubClassOf(memoryDir, retractSubject, retractObject);
|
|
2876
|
+
if (result.found) {
|
|
2877
|
+
const extra = result.count - 1; // beyond the target fact itself
|
|
2878
|
+
return {
|
|
2879
|
+
text: `noted — forgotten: "${retractSubject} is a kind of ${retractObject}" is no longer stored`
|
|
2880
|
+
+ (extra > 0 ? ` (${extra} entailed fact${extra === 1 ? "" : "s"} that depended on it went too)` : "")
|
|
2881
|
+
+ (result.truncated ? " — this cascade may not be complete (a lot depended on it); ask again if something still looks stale" : "")
|
|
2882
|
+
+ ".",
|
|
2883
|
+
via: "retract", miss: false,
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
// found:false — fall through to the rest of the cascade (see docblock above).
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2787
2889
|
// OWNERSHIP — "<Name> owns/maintains <X>", bare or remember-wrapped. The bare
|
|
2788
2890
|
// form is double-gated: no interrogative lead, PLUS either side spelling a
|
|
2789
2891
|
// Capitalized token — so the "who owns <X>" READ question and ordinary
|
|
@@ -4317,6 +4419,37 @@ const RECURSIVE_LIST_ASK_RE = /^list\s+(?:the\s+|all\s+)?([a-z][\w-]*)\s+of\s+([
|
|
|
4317
4419
|
const ISA_ASK_RE = /^(?:is|are)\s+(?:an?\s+)?(.+?)\s+(?:a\s+kind\s+of|a\s+type\s+of|an?)\s+(.+?)[?.!\s]*$/i;
|
|
4318
4420
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
4319
4421
|
|
|
4422
|
+
/** "why is TaskController a handler" / "explain how you know TaskController is
|
|
4423
|
+
* a handler" — BENCHMARK_CONVERSATION_1.8.14.md item 9 (skeptical-power-user
|
|
4424
|
+
* persona): the syllogise-verified proof render (the isaAsk block below,
|
|
4425
|
+
* which cites the graph inherits-bridge / taught-fact chase / entailed
|
|
4426
|
+
* closure) only ever fired on the bare "is X a Y" yes/no form — a "why"/
|
|
4427
|
+
* "explain how you know" wrapper around the EXACT SAME question hit the bare
|
|
4428
|
+
* wall instead, even though the underlying answer is real and sourced.
|
|
4429
|
+
* Deliberately NOT a new answer path — a pure text rewrite back onto
|
|
4430
|
+
* ISA_ASK_RE's own "is X a Y" shape, mirroring CONFIRM_TAG_RE's own
|
|
4431
|
+
* "rewrite the wrapper away and re-try ISA_ASK_RE" approach just below (and
|
|
4432
|
+
* reused at both this file's ISA_ASK_RE match sites, the memory-only isa
|
|
4433
|
+
* check and the graph-grounded proof-chase). "why is X a Y" already leads
|
|
4434
|
+
* with "is"/"are" (ISA_ASK_RE's own anchor) — only the "why " lead needs
|
|
4435
|
+
* stripping; "explain how you know X is Y" leads with the SUBJECT in plain
|
|
4436
|
+
* declarative order, so it's reordered into "is X a Y" the same way
|
|
4437
|
+
* CONFIRM_TAG_RE reorders its own "X is Y, right?" tag. Returns ISA_ASK_RE's
|
|
4438
|
+
* own match array (or null) — a caller never needs to know which of the two
|
|
4439
|
+
* shapes fired, same discipline as `isaAsk` already applies to CONFIRM_TAG_RE. */
|
|
4440
|
+
const WHY_ISA_LEAD_RE = /^why\s+(?=(?:is|are)\b)/i;
|
|
4441
|
+
const EXPLAIN_HOW_YOU_KNOW_RE = /^explain\s+how\s+you\s+know\s+(?:that\s+)?(.+?)\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
4442
|
+
function matchWhyIsa(q) {
|
|
4443
|
+
const stripped = String(q || "").replace(WHY_ISA_LEAD_RE, "");
|
|
4444
|
+
if (stripped !== q) {
|
|
4445
|
+
const m = stripped.match(ISA_ASK_RE);
|
|
4446
|
+
if (m) return m;
|
|
4447
|
+
}
|
|
4448
|
+
const ehyk = String(q || "").match(EXPLAIN_HOW_YOU_KNOW_RE);
|
|
4449
|
+
if (ehyk) return `is ${ehyk[1].trim()} a ${ehyk[2].trim()}`.match(ISA_ASK_RE);
|
|
4450
|
+
return null;
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4320
4453
|
// Live-caught 2026-07-11 (follow-up to the "what is a kind of X" ambiguousParse
|
|
4321
4454
|
// fix, commit 5c858bf): RELATION_FACT_YESNO_RE/RELATION_WHO_ASK_RE both capture a
|
|
4322
4455
|
// middle "role" word and treat it as an arbitrary user-taught relation/rule NAME
|
|
@@ -4483,7 +4616,8 @@ async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}) {
|
|
|
4483
4616
|
if (!miss) return null;
|
|
4484
4617
|
|
|
4485
4618
|
// (b) "is a module a component" — yes iff a remembered isa-family fact says so.
|
|
4486
|
-
|
|
4619
|
+
// Also accepts "why is X a Y" / "explain how you know X is Y" — see matchWhyIsa.
|
|
4620
|
+
const isa = q.match(ISA_ASK_RE) || matchWhyIsa(q);
|
|
4487
4621
|
if (isa) {
|
|
4488
4622
|
const subj = factTermVariants(normFactTerm, isa[1]);
|
|
4489
4623
|
const obj = factTermVariants(normFactTerm, isa[2]);
|
|
@@ -5480,7 +5614,11 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
|
|
|
5480
5614
|
// plain "is X a Y" form and re-tried when the raw query itself doesn't match —
|
|
5481
5615
|
// see CONFIRM_TAG_RE's own docblock.
|
|
5482
5616
|
const confirmTag = q.match(CONFIRM_TAG_RE);
|
|
5483
|
-
|
|
5617
|
+
// "why is X a Y" / "explain how you know X is Y" (BENCHMARK_CONVERSATION_1.8.14.md
|
|
5618
|
+
// item 9) reach the SAME sourced/verified proof chase below via matchWhyIsa's
|
|
5619
|
+
// rewrite — see its own docblock.
|
|
5620
|
+
const isaAsk = q.match(ISA_ASK_RE) || matchWhyIsa(q)
|
|
5621
|
+
|| (confirmTag && `is ${confirmTag[1].trim()} a ${confirmTag[2].trim()}`.match(ISA_ASK_RE));
|
|
5484
5622
|
if (isaAsk) {
|
|
5485
5623
|
// Playtest sprint round 1 (2026-07-10): "is TaskController a validator then"
|
|
5486
5624
|
// — the same trailing bare discourse tag item 8 fixed for metaTermOf's bare
|
|
@@ -6854,20 +6992,33 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
6854
6992
|
|
|
6855
6993
|
/** COMPARE (HANDOVER.md 2026-07-12 "no comparison capability" item) — a scoped
|
|
6856
6994
|
* v1: "how is X different from Y", "how does X differ from Y", "compare X and
|
|
6857
|
-
* Y"/"compare X with/to Y", "what's the difference between X and Y".
|
|
6858
|
-
*
|
|
6995
|
+
* Y"/"compare X with/to Y", "what's the difference between X and Y". Closed
|
|
6996
|
+
* patterns, same discipline as DESCRIBE_WRAPPER_RE/DETAILED_HOW_WORKS_RE
|
|
6859
6997
|
* above — curated anchors, never a general "any two nouns" catch-all. Named
|
|
6860
6998
|
* capture groups (a/b) so compareAnswer doesn't need to know which pattern
|
|
6861
6999
|
* fired. Tried as a LAST-RESORT rescue (same call-site discipline as (4d)/(4e)
|
|
6862
7000
|
* below) since neither ask.mjs's compositional grammar nor any existing lane
|
|
6863
7001
|
* recognizes a two-entity comparison at all — there is nothing for this to
|
|
6864
|
-
* shadow.
|
|
7002
|
+
* shadow.
|
|
7003
|
+
*
|
|
7004
|
+
* BENCHMARK_CONVERSATION_1.8.14.md item 8 (rushed-dev persona): "how is Task
|
|
7005
|
+
* diff from User" / "whats the diff between TaskController and
|
|
7006
|
+
* UserController" both hit the bare wall — "diff" is a common casual synonym
|
|
7007
|
+
* for "different"/"difference" this table never accepted. Folded in as an
|
|
7008
|
+
* additional alternation on the two patterns it naturally pairs with (never
|
|
7009
|
+
* a new standalone pattern — "diff" means exactly what "different"/
|
|
7010
|
+
* "difference" already mean here, so it rides the SAME two anchors). The
|
|
7011
|
+
* "what's the diff between" anchor also picks up the bare no-apostrophe
|
|
7012
|
+
* "whats" contraction spelling (`what(?:'s|s|\s+is)`) while here — the same
|
|
7013
|
+
* tolerance MODULE_ORIENT_RE/MODULE_PURPOSE_RE's own docblock already
|
|
7014
|
+
* documents elsewhere in this file — since the persona's own verbatim input
|
|
7015
|
+
* used exactly that spelling. */
|
|
6865
7016
|
const COMPARE_PATTERNS = [
|
|
6866
|
-
/^how\s+(?:is|are)\s+(?<a>.+?)\s+different\s+from\s+(?<b>.+?)$/i,
|
|
7017
|
+
/^how\s+(?:is|are)\s+(?<a>.+?)\s+(?:different|diff)\s+from\s+(?<b>.+?)$/i,
|
|
6867
7018
|
/^how\s+do(?:es)?\s+(?<a>.+?)\s+differ\s+from\s+(?<b>.+?)$/i,
|
|
6868
7019
|
/^how\s+are\s+(?<a>.+?)\s+and\s+(?<b>.+?)\s+different$/i,
|
|
6869
7020
|
/^compare\s+(?<a>.+?)\s+(?:and|with|to)\s+(?<b>.+?)$/i,
|
|
6870
|
-
/^(?:what(?:'s|\s+is)\s+the\s+difference\s+between|difference\s+between)\s+(?<a>.+?)\s+and\s+(?<b>.+?)$/i,
|
|
7021
|
+
/^(?:what(?:'s|s|\s+is)\s+the\s+(?:difference|diff)\s+between|(?:difference|diff)\s+between)\s+(?<a>.+?)\s+and\s+(?<b>.+?)$/i,
|
|
6871
7022
|
];
|
|
6872
7023
|
|
|
6873
7024
|
/** Strip a leading article — resolveSymbol (codegraph.mjs) has no article
|
|
@@ -7391,7 +7542,41 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7391
7542
|
}
|
|
7392
7543
|
}
|
|
7393
7544
|
const answeredIds = (envelope?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
7394
|
-
const
|
|
7545
|
+
const askMiss = envelope ? !!envelope.miss : true;
|
|
7546
|
+
// GRAPH-FACT-VS-TAUGHT-FACT PRECEDENCE (PLAN_SYLLOGIST.md finding 2 fix,
|
|
7547
|
+
// 2026-07-12): a plain declarative "X is a kind of Y" sentence whose SUBJECT
|
|
7548
|
+
// already names a real graph entity could get silently "answered" by the ask
|
|
7549
|
+
// engine's own relaxation cascade instead of ever reaching the teach lane.
|
|
7550
|
+
// Root cause, verified live against examples/mini-webapp: relaxParse's
|
|
7551
|
+
// DROP-UNMATCHED layer (src/ask.mjs "Layer 2") drops an unresolvable trailing
|
|
7552
|
+
// content word ("animal" — not a graph entity) entirely, turning "a Task is a
|
|
7553
|
+
// kind of animal" into the DIFFERENT, valid elliptical question "a Task is a
|
|
7554
|
+
// kind of" (a forward "inherits" ask), which genuinely answers "Record" (Task
|
|
7555
|
+
// really does inherit Record in the code graph) — envelope.miss comes back
|
|
7556
|
+
// false, so the miss-gated TEACH lane (4) below never even ran. That graph
|
|
7557
|
+
// answer must keep working as a QUESTION ("what is Task a kind of" must still
|
|
7558
|
+
// say Record) — the actual bug is only that a DECLARATIVE sentence
|
|
7559
|
+
// (subject-first, no question lead) with a genuine content word dropped by
|
|
7560
|
+
// relaxation never got a chance to be taught at all. Detected narrowly (all
|
|
7561
|
+
// three must hold): the ask engine's answer came via envelope.relaxed
|
|
7562
|
+
// (something was dropped/repaired, not a clean direct hit), the raw query is
|
|
7563
|
+
// NOT phrased as a question (QUESTION_LEAD_RE, no trailing "?"), and it
|
|
7564
|
+
// independently fits the closed "X is a kind of Y" teach shape
|
|
7565
|
+
// (DECLARATIVE_KIND_OF_RE). When all three hold, this turn is treated as a
|
|
7566
|
+
// would-miss so the ordinary miss-gated cascade below (including the
|
|
7567
|
+
// existing TEACH lane (4)) gets a real turn at it — a genuinely stored taught
|
|
7568
|
+
// fact wins over a repaired-away graph reading, COEXISTING with it rather
|
|
7569
|
+
// than replacing it: the graph fact itself is never touched and stays
|
|
7570
|
+
// answerable through its own question phrasing. If nothing below actually
|
|
7571
|
+
// stores it (recordMiss still true once the whole would-miss cascade has
|
|
7572
|
+
// run), the ORIGINAL ask-engine answer is restored unchanged just before the
|
|
7573
|
+
// record is built (see "COLLISION RESTORE" below) — never worse than today.
|
|
7574
|
+
const trimmedQuery = String(query).trim();
|
|
7575
|
+
const relaxedTeachCollision = !!(envelope?.relaxed?.dropped?.length) && !askMiss && memoryDir
|
|
7576
|
+
&& !QUESTION_LEAD_RE.test(trimmedQuery) && !/\?\s*$/.test(trimmedQuery)
|
|
7577
|
+
&& DECLARATIVE_KIND_OF_RE.test(trimmedQuery);
|
|
7578
|
+
const preCollisionAnswer = relaxedTeachCollision ? answer : null;
|
|
7579
|
+
const miss = askMiss || relaxedTeachCollision;
|
|
7395
7580
|
// Answer provenance (W1): "composed" is the ask engine's productive band; the
|
|
7396
7581
|
// orientation swap below is template wording, so those turns carry via:"template".
|
|
7397
7582
|
let via = "composed";
|
|
@@ -7438,6 +7623,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7438
7623
|
// itself already uses for a normally-parsed relation query, so the two
|
|
7439
7624
|
// never disagree on the cases where both would fire).
|
|
7440
7625
|
let deduced = deduceGoalFromParsed(envelope?.parsed);
|
|
7626
|
+
// Paired with preCollisionAnswer above: the goal line that matched the
|
|
7627
|
+
// ORIGINAL ask-engine answer, restored alongside it (COLLISION RESTORE,
|
|
7628
|
+
// below) if the would-miss cascade below never actually stores anything —
|
|
7629
|
+
// so a restored graph answer never carries a stale "teach a new fact" goal.
|
|
7630
|
+
const preCollisionDeduced = relaxedTeachCollision ? deduced : null;
|
|
7441
7631
|
note(trace, `goal: ${deduced ?? "unclear — the phrasing didn't resolve to a known query shape"}`);
|
|
7442
7632
|
// MISS handling. The intent lanes + short-miss are RECOGNIZER-gated on the query
|
|
7443
7633
|
// text AND only consulted on a would-miss, so a real graph query — a hit, an honest
|
|
@@ -7596,8 +7786,25 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7596
7786
|
// factAnswer itself declines for this shape (no metaTerm), so the only
|
|
7597
7787
|
// change in practice is that factReadBack's (a2c) property lane gets a
|
|
7598
7788
|
// chance to run before the orientation card claims the turn.
|
|
7599
|
-
|
|
7600
|
-
|
|
7789
|
+
// BENCHMARK_CONVERSATION_1.8.14.md item 11: "hey quick q, what is TaskController"
|
|
7790
|
+
// (a leading filler clause) and "whats UserController" (a no-apostrophe
|
|
7791
|
+
// contraction) both used to test the RAW `query` text against BARE_WHATIS_RE/
|
|
7792
|
+
// IS_ADJECTIVE_YESNO_RE's own anchored `^what`/`^is` gate directly — so neither
|
|
7793
|
+
// ever reached this lane at all, even though normalizeQuery (already imported
|
|
7794
|
+
// here, same NO_FOCUS_WHATS_IN_HERE_RE precedent above in this file) already
|
|
7795
|
+
// resolves both: its CONTRACTIONS table expands "whats" -> "what is", and its
|
|
7796
|
+
// stripFillerWords pass (commit 282c010, now covering "quick q" too — see
|
|
7797
|
+
// FILLER_WORDS' own docblock, ask-vocab.mjs) peels the leading "hey quick q,"
|
|
7798
|
+
// clause. Composed from those two EXISTING mechanisms rather than a third:
|
|
7799
|
+
// `gateQuery` is normalizeQuery's output, used for the shape gate AND the
|
|
7800
|
+
// lookups it feeds (metaTermOf/factAnswer/factReadBack/curatedDefinitionAnswer)
|
|
7801
|
+
// so a filler-wrapped/no-apostrophe query resolves to the SAME real answer its
|
|
7802
|
+
// clean form already does — never a new answer path. Idempotent on already-
|
|
7803
|
+
// clean text (normalizeQuery's own contract), so the ordinary case is
|
|
7804
|
+
// byte-unchanged.
|
|
7805
|
+
const gateQuery = normalizeQuery(String(query));
|
|
7806
|
+
const bareWhatisShape = BARE_WHATIS_RE.test(gateQuery);
|
|
7807
|
+
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(gateQuery);
|
|
7601
7808
|
// HANDOVER.md 2026-07-12 CamelCase finding: `isBareCamelCaseMetaQuestion` (see
|
|
7602
7809
|
// its own docblock, above isConversational) OR's in alongside
|
|
7603
7810
|
// isConversationalCandidate for THIS lane only — a bare "what is TaskController"
|
|
@@ -7609,12 +7816,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7609
7816
|
// alone: the `else if (isConversationalCandidate)` orientation-card fallback
|
|
7610
7817
|
// further down is UNCHANGED, so a CamelCase term with no real hit still falls
|
|
7611
7818
|
// through to its existing miss handling, never the generic orientation card.
|
|
7612
|
-
const isBareCamelCaseWhatisCandidate = conversationalCandidateBaseGate && isBareCamelCaseMetaQuestion(
|
|
7819
|
+
const isBareCamelCaseWhatisCandidate = conversationalCandidateBaseGate && isBareCamelCaseMetaQuestion(gateQuery);
|
|
7613
7820
|
let bareMetaHit = null;
|
|
7614
7821
|
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape)) {
|
|
7615
7822
|
if (memoryDir) {
|
|
7616
|
-
bareMetaHit = (await factAnswer(memoryDir,
|
|
7617
|
-
?? (await factReadBack(memoryDir,
|
|
7823
|
+
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle))
|
|
7824
|
+
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
7618
7825
|
// HANDOVER.md 2026-07-10 item 10 (dropped-article gap): a bare "what is X"
|
|
7619
7826
|
// with NO taught fact but a KNOWN curated corpus term ("what is cache", no
|
|
7620
7827
|
// article) used to lose this exact same isConversationalCandidate race —
|
|
@@ -7624,7 +7831,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7624
7831
|
// lane — an unknown bare term still falls through to the ordinary
|
|
7625
7832
|
// orientation card, exactly as before.
|
|
7626
7833
|
if (!bareMetaHit) {
|
|
7627
|
-
const def = await curatedDefinitionAnswer(
|
|
7834
|
+
const def = await curatedDefinitionAnswer(gateQuery, envelope, { memoryDir, lexicon });
|
|
7628
7835
|
if (def) bareMetaHit = { text: def.text, replace: true };
|
|
7629
7836
|
}
|
|
7630
7837
|
}
|
|
@@ -7641,7 +7848,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7641
7848
|
// graph but no memoryDir at all, so gating this on memoryDir too would
|
|
7642
7849
|
// silently never fire in the one harness this fix specifically targets.
|
|
7643
7850
|
if (!bareMetaHit && graph) {
|
|
7644
|
-
const term = metaTermOf(
|
|
7851
|
+
const term = metaTermOf(gateQuery, envelope);
|
|
7645
7852
|
if (term) {
|
|
7646
7853
|
const { metaFallbackEntityAnswer } = await import("./ask.mjs");
|
|
7647
7854
|
const fallback = metaFallbackEntityAnswer(graph, term);
|
|
@@ -8033,6 +8240,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8033
8240
|
}
|
|
8034
8241
|
}
|
|
8035
8242
|
}
|
|
8243
|
+
// COLLISION RESTORE (pairs with relaxedTeachCollision, above): the whole
|
|
8244
|
+
// would-miss cascade above (TEACH lane included) has now had its turn. If
|
|
8245
|
+
// nothing in it actually stored/answered anything (recordMiss is still
|
|
8246
|
+
// true), fall back to the ORIGINAL ask-engine answer computed before this
|
|
8247
|
+
// turn was forced into the would-miss cascade — a real graph answer beats
|
|
8248
|
+
// any generic miss/wall text, and this is never worse than the turn would
|
|
8249
|
+
// have rendered before this fix existed. If the TEACH lane (or any other
|
|
8250
|
+
// lane) DID answer for real, recordMiss is already false here and this is a
|
|
8251
|
+
// no-op — the taught fact's own confirmation stands, coexisting with the
|
|
8252
|
+
// (untouched) graph fact.
|
|
8253
|
+
if (relaxedTeachCollision && recordMiss) {
|
|
8254
|
+
answer = preCollisionAnswer;
|
|
8255
|
+
via = "composed";
|
|
8256
|
+
recordMiss = false;
|
|
8257
|
+
deduced = preCollisionDeduced;
|
|
8258
|
+
note(trace, "lane: COLLISION RESTORE — the teach-shaped declarative wasn't storable after all; the original ask-engine (graph) answer stands, untouched");
|
|
8259
|
+
}
|
|
8036
8260
|
// W5 (flag-gated, default OFF): an unknown-term miss may consult the LOCAL
|
|
8037
8261
|
// committed corpus slice — a hit APPENDS a grounded, licence-cited aside under
|
|
8038
8262
|
// the honest miss (the miss itself stands; the aside is context, not an answer).
|