@polycode-projects/the-mechanical-code-talker 1.8.11 → 1.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -12
- package/ROADMAP.md +9 -135
- package/data/templates/grammar-rules.toml +1 -1
- package/package.json +1 -1
- package/src/ask-browser-entry.mjs +0 -8
- package/src/ask-browser.bundle.js +68 -4
- package/src/ask-nlp.mjs +0 -9
- package/src/ask.mjs +217 -39
- package/src/chat.mjs +320 -11
- package/src/codegraph.mjs +75 -0
- package/src/interpret/normalize.mjs +45 -7
- package/src/memory/core.mjs +63 -0
- package/src/paraphrase.mjs +131 -0
- package/src/router/planner.mjs +0 -4
- package/src/server.mjs +8 -1
- package/src/source-slice.mjs +1 -2
- package/src/syllogise.mjs +160 -1
package/src/chat.mjs
CHANGED
|
@@ -43,10 +43,10 @@ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
|
43
43
|
import { tmpdir } from "node:os";
|
|
44
44
|
import { createInterface } from "node:readline/promises";
|
|
45
45
|
import { spawnSync } from "node:child_process";
|
|
46
|
-
import { dispatchTool } from "./server.mjs";
|
|
46
|
+
import { dispatchTool, loadGraph } from "./server.mjs";
|
|
47
47
|
import { loadConfig, DEFAULT_GRAPH_REL } from "./config.mjs";
|
|
48
48
|
import { resolveRuntimeConfig } from "./cli-args.mjs";
|
|
49
|
-
import { parseEntities, edgesOfKind, renderAuthorCard, renderAuthorTouches, renderCommitAuthor } from "./codegraph.mjs";
|
|
49
|
+
import { parseEntities, edgesOfKind, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "./codegraph.mjs";
|
|
50
50
|
import { SESSIONS_DIR_REL, appendSessionToGraph } from "./sessions.mjs";
|
|
51
51
|
import { uuidv7 } from "./uuid.mjs";
|
|
52
52
|
import { createTelemetry } from "./telemetry.mjs";
|
|
@@ -57,9 +57,9 @@ import { rankByBiasThenTrust } from "./memory/bias.mjs";
|
|
|
57
57
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
58
58
|
import {
|
|
59
59
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
60
|
-
stripTrailingScopeFiller, stripTrailingDiscourseTag,
|
|
60
|
+
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS,
|
|
61
61
|
} from "./ask-vocab.mjs";
|
|
62
|
-
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, normalizeQuery, escapeRegex, kindNounAnaphoraHint } from "./interpret/normalize.mjs";
|
|
62
|
+
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, normalizeQuery, stripFillerWords, escapeRegex, kindNounAnaphoraHint } from "./interpret/normalize.mjs";
|
|
63
63
|
import { fuzzyMatchInSet, fuzzyBound } from "./interpret/fuzzy.mjs";
|
|
64
64
|
import { pickPhrase } from "./answer-variants.mjs";
|
|
65
65
|
|
|
@@ -564,6 +564,113 @@ export function answerCount(graph, query) {
|
|
|
564
564
|
return `${n} ${classNoun(cls, n)}.`;
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
/** singular+plural display forms for the edge-nominalized nouns answerEdgeCount
|
|
568
|
+
* (below) actually answers — a small subset of EDGE_NOUN_TO_METRIC's keys, the
|
|
569
|
+
* ones that read as a real countable noun ("3 callers.") rather than a
|
|
570
|
+
* participle only natural in a superlative ("most USED", not "how many used").
|
|
571
|
+
* A key with no entry here echoes the user's own word unchanged (safe default,
|
|
572
|
+
* same fallback CLASS_LABELS/classNoun use above). */
|
|
573
|
+
const EDGE_NOUN_LABELS = {
|
|
574
|
+
test: ["test", "tests"], tests: ["test", "tests"],
|
|
575
|
+
importers: ["importer", "importers"], dependents: ["dependent", "dependents"],
|
|
576
|
+
callers: ["caller", "callers"], callees: ["callee", "callees"],
|
|
577
|
+
calls: ["call", "calls"], imports: ["import", "imports"],
|
|
578
|
+
dependencies: ["dependency", "dependencies"], members: ["member", "members"],
|
|
579
|
+
subclasses: ["subclass", "subclasses"], connections: ["connection", "connections"],
|
|
580
|
+
edges: ["edge", "edges"],
|
|
581
|
+
};
|
|
582
|
+
const edgeCountNoun = (noun, n) => {
|
|
583
|
+
const [s, p] = EDGE_NOUN_LABELS[noun] || [noun, noun];
|
|
584
|
+
return n === 1 ? s : p;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/** Pull the named entity out of a per-entity edge-count tail — the text after
|
|
588
|
+
* "how many <edge-noun>" — recognising exactly the two closed shapes such a
|
|
589
|
+
* tail actually takes:
|
|
590
|
+
* - "<verb> <entity>" ("cover src/x.mjs", "import Widget") — verb drawn
|
|
591
|
+
* from ask-vocab.mjs's RELATIONS[metric.kind].verbs (the SAME verb list
|
|
592
|
+
* the relation clause grammar itself reads), longest-first so a
|
|
593
|
+
* multi-word verb ("depends on") matches whole rather than a short
|
|
594
|
+
* prefix stealing part of the entity name.
|
|
595
|
+
* - "does/do/did <entity> have/has/had/got" ("does X have") — safe to
|
|
596
|
+
* treat as unambiguous HERE even though answerCount's own
|
|
597
|
+
* AMBIGUOUS_HAVE_VERBS guard deliberately excludes "have" elsewhere:
|
|
598
|
+
* that guard exists because "have" maps to either defines/contains
|
|
599
|
+
* depending on the SUBJECT's class, but an edge-nominalized noun's
|
|
600
|
+
* metric.dir is fixed by the NOUN itself ("importers" is always dir
|
|
601
|
+
* "in"), so there is no analogous ambiguity to worry about here.
|
|
602
|
+
* Returns the trimmed entity term, or null (no recognizable shape — an
|
|
603
|
+
* honest decline, not a guess) — the caller then leaves the existing "I
|
|
604
|
+
* can't count" message from answerCount standing. */
|
|
605
|
+
function extractEdgeCountEntity(tail, metric) {
|
|
606
|
+
const t = String(tail || "").trim().replace(/[?.!]+$/, "").trim();
|
|
607
|
+
if (!t) return null;
|
|
608
|
+
const haveM = t.match(/^(?:does|do|did)\s+(.+?)\s+(?:have|has|had|got)$/i);
|
|
609
|
+
if (haveM && haveM[1].trim()) return haveM[1].trim();
|
|
610
|
+
if (metric.kind !== "*" && RELATIONS[metric.kind]) {
|
|
611
|
+
const verbs = [...RELATIONS[metric.kind].verbs].sort((a, b) => b.length - a.length);
|
|
612
|
+
for (const v of verbs) {
|
|
613
|
+
const re = new RegExp(`^${v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+(.+)$`, "i");
|
|
614
|
+
const m = t.match(re);
|
|
615
|
+
if (m && m[1].trim()) return m[1].trim();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Bare "how many <edge-noun> <verb> <entity>" / "how many <edge-noun> does
|
|
622
|
+
* <entity> have" (HANDOVER "bare 'how many X' fails for edge-nominalized
|
|
623
|
+
* nouns", 2026-07-12 fix) — "how many tests cover X", "how many importers
|
|
624
|
+
* does X have", "how many callers does X have". answerCount's own COUNT_NOUNS
|
|
625
|
+
* table only maps a counted noun to a graph INDIVIDUAL CLASS (Module/Class/…);
|
|
626
|
+
* an edge-nominalized noun like "tests"/"importers"/"callers" names an EDGE
|
|
627
|
+
* KIND instead — ask-vocab.mjs's EDGE_NOUN_TO_METRIC, the SAME table the
|
|
628
|
+
* working superlative lane ("which module has the most tests") already reads
|
|
629
|
+
* — so it was never in COUNT_NOUNS and answerCount short-circuited straight
|
|
630
|
+
* to "I can't count 'tests'" without ever consulting that table. This reuses
|
|
631
|
+
* the SAME per-entity degree computation the superlative lane's own
|
|
632
|
+
* evalSuperlative uses to rank every entity of a class (ask.mjs's
|
|
633
|
+
* degreeMetric, exported for exactly this) — just read for the ONE named
|
|
634
|
+
* entity instead of sorting all of them.
|
|
635
|
+
*
|
|
636
|
+
* Checked BEFORE answerCount in runTurn (same precedence pattern as
|
|
637
|
+
* answerMemoryCount/answerQuantifierRecall above) so it gets first look;
|
|
638
|
+
* declines (returns null, letting answerCount's existing message stand)
|
|
639
|
+
* whenever:
|
|
640
|
+
* - the noun isn't edge-nominalized at all (a COUNT_NOUNS class, or truly
|
|
641
|
+
* unknown), or
|
|
642
|
+
* - no entity term could be extracted from the tail
|
|
643
|
+
* (extractEdgeCountEntity), or
|
|
644
|
+
* - the extracted term doesn't resolve to exactly one graph entity.
|
|
645
|
+
*
|
|
646
|
+
* SCOPED to the per-entity case only: a bare "how many tests are there" (no
|
|
647
|
+
* named entity in the tail) has nothing for extractEdgeCountEntity to pull
|
|
648
|
+
* out, so it declines here and keeps answerCount's existing "I can't count
|
|
649
|
+
* 'tests'" honest miss — what a GLOBAL edge count would even mean (every
|
|
650
|
+
* test edge in the graph? distinct test modules? distinct tested modules?)
|
|
651
|
+
* is a genuine, undecided design question, out of this fix's scope. */
|
|
652
|
+
async function answerEdgeCount(graph, query) {
|
|
653
|
+
if (!graph) return null;
|
|
654
|
+
const q = String(query);
|
|
655
|
+
if (ANAPHORA_COUNT_RE.test(q) || IMPLICIT_ANAPHORA_COUNT_RE.test(q.trim())) return null;
|
|
656
|
+
const m = q.match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b/i);
|
|
657
|
+
if (!m) return null;
|
|
658
|
+
const noun = m[1].toLowerCase();
|
|
659
|
+
if (COUNT_NOUNS[noun]) return null; // a real graph class — answerCount owns it
|
|
660
|
+
const metric = EDGE_NOUN_TO_METRIC[noun];
|
|
661
|
+
if (!metric) return null; // not edge-nominalized either — answerCount's "I can't count" stands
|
|
662
|
+
const term = extractEdgeCountEntity(q.slice(m.index + m[0].length), metric);
|
|
663
|
+
if (!term) return null; // no per-entity phrasing recognized — scoped out (see docblock)
|
|
664
|
+
const entity = await resolveEntity(graph, term);
|
|
665
|
+
if (!entity) return null; // unresolved/ambiguous entity — honest decline, not a guess
|
|
666
|
+
const ind = graph.byId?.get?.(entity.id);
|
|
667
|
+
if (!ind) return null;
|
|
668
|
+
let degreeMetric;
|
|
669
|
+
try { ({ degreeMetric } = await import("./ask.mjs")); } catch { return null; }
|
|
670
|
+
const n = degreeMetric(graph, ind, metric);
|
|
671
|
+
return `${n} ${edgeCountNoun(noun, n)}.`;
|
|
672
|
+
}
|
|
673
|
+
|
|
567
674
|
/** ASSERTED-VOCABULARY count (CHATBENCH_006 lever 3): once "every class is a type"
|
|
568
675
|
* is remembered, "how many types are there" counts as many types as there are
|
|
569
676
|
* classes — the asserted object noun inherits the subject class's cardinality.
|
|
@@ -869,6 +976,29 @@ export function isConversational(query) {
|
|
|
869
976
|
return q.split(/\s+/).filter(Boolean).length <= 3 && !codeish;
|
|
870
977
|
}
|
|
871
978
|
|
|
979
|
+
/** Scoped exemption for the bare-meta-fact lane (2b/2c, further down this file)
|
|
980
|
+
* ONLY — never a change to looksCodeish()/isConversational() themselves, and
|
|
981
|
+
* never used for the generic orientation-card fallback. HANDOVER.md 2026-07-12
|
|
982
|
+
* finding: a bare "what is TaskController?" (CamelCase COMPOUND class name, no
|
|
983
|
+
* article) hits looksCodeish()'s `/[a-z][A-Z]/` branch, so isConversational()
|
|
984
|
+
* returns false and the whole isConversationalCandidate gate — including the
|
|
985
|
+
* bare-meta-fact lookup that "what is a TaskController" (articled, via its own
|
|
986
|
+
* T5 structural parse) already resolves through — never runs. Single-word/
|
|
987
|
+
* lowercased names ("Widget", "taskcontroller") have no lowercase-to-uppercase
|
|
988
|
+
* transition so they were never affected; only two-word-shaped compounds were.
|
|
989
|
+
* This re-tests the SAME non-CamelCase codeish reasons (paths, dotted refs,
|
|
990
|
+
* `()` calls, STRUCT_WORDS) looksCodeish already covers, so a genuine near-miss
|
|
991
|
+
* structural question ("what is foo.bar()", "what is import") is UNCHANGED —
|
|
992
|
+
* still excluded here, still falls through to its existing (better) miss
|
|
993
|
+
* handling, never the friendly orientation card. */
|
|
994
|
+
function isBareCamelCaseMetaQuestion(query) {
|
|
995
|
+
const raw = String(query).trim();
|
|
996
|
+
const q = raw.toLowerCase().replace(/[.!?]+$/, "").trim();
|
|
997
|
+
const nonCamelCodeish = /[_./]|\(\)/.test(raw) || q.split(/\s+/).some((w) => STRUCT_WORDS.has(w));
|
|
998
|
+
if (nonCamelCodeish || q.split(/\s+/).filter(Boolean).length > 3) return false;
|
|
999
|
+
return BARE_WHATIS_RE.test(raw) || IS_ADJECTIVE_YESNO_RE.test(raw);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
872
1002
|
// ---- the response-template library (W1: templates → render path) ----
|
|
873
1003
|
// The WORDING of the conversational/orientation surfaces lives in
|
|
874
1004
|
// data/templates/responses.jsonl (corpus/templates.mjs) — the template library is
|
|
@@ -3076,7 +3206,17 @@ async function memorySummary(memoryDir, graph) {
|
|
|
3076
3206
|
// both already exist. CASE-PRESERVING: module paths/symbol names are
|
|
3077
3207
|
// case-sensitive, so this reads the ORIGINAL query text, never metaLane's
|
|
3078
3208
|
// lowercased `q` (authorLane's same discipline, just above/below).
|
|
3079
|
-
|
|
3209
|
+
/** A trailing intensifier/filler adverb tacked onto "do"/"does" ("what does the
|
|
3210
|
+
* store module do exactly?", "...do exactly", "what X does really") — fast
|
|
3211
|
+
* loop round 8 (ESL/filler-phrasing angle): the closed-form anchor below used
|
|
3212
|
+
* to require "do"/"does" to be the LAST word before the optional "?", so this
|
|
3213
|
+
* one extra word past it hit the raw grammar wall even though the shared
|
|
3214
|
+
* FILLER_WORDS/normalizeQuery pass (used elsewhere in the file) never sees
|
|
3215
|
+
* this lane's case-preserving text at all. Mirrors MODULE_ORIENT_POLITENESS_RE
|
|
3216
|
+
* just below: closed, optional, single-lane blast radius — a bare "what does
|
|
3217
|
+
* X do" still matches with this suffix empty. */
|
|
3218
|
+
const TRAILING_ADVERB_RE = "(?:\\s+(?:exactly|really|actually|anyway))?";
|
|
3219
|
+
const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)\\s+do${TRAILING_ADVERB_RE}\\??$`, "i");
|
|
3080
3220
|
/** The SUBJECT-FIRST word order of the SAME question ("what saveStore does" vs
|
|
3081
3221
|
* "what does saveStore do") — Tier 6 playtest, §3b surface-variation axis: a
|
|
3082
3222
|
* perfectly natural alternate phrasing of an ALREADY-recognized intent that
|
|
@@ -3086,7 +3226,7 @@ const MODULE_ORIENT_RE = /^what\s+does\s+(.+?)\s+do\??$/i;
|
|
|
3086
3226
|
* UNIQUE graph entity or this lane declines) is what keeps this loose an
|
|
3087
3227
|
* ending safe — a syntactic match against a term that isn't a real entity
|
|
3088
3228
|
* simply falls through unchanged, same as every other lane in this file. */
|
|
3089
|
-
const MODULE_ORIENT_SVO_RE =
|
|
3229
|
+
const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB_RE}\\??$`, "i");
|
|
3090
3230
|
// Seonix Batch 3 (3a) — purpose/identity phrasing: "whats X for"/"what's X
|
|
3091
3231
|
// about"/"what is X for", the sibling of "what does X do" that asks for the
|
|
3092
3232
|
// SAME module-grain overview. Deliberately does NOT claim the literal noun
|
|
@@ -3123,10 +3263,22 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
3123
3263
|
// do") — plus a lane-local politeness strip for "please explain X" (applyPreambleFrames's
|
|
3124
3264
|
// own EXPLAIN_WRAPPER_RE requires the string to literally START with "explain",
|
|
3125
3265
|
// so a LEADING "please"/"kindly" ahead of it defeats that frame; see
|
|
3126
|
-
// MODULE_ORIENT_POLITENESS_RE's own docblock). All
|
|
3266
|
+
// MODULE_ORIENT_POLITENESS_RE's own docblock). All four are additive,
|
|
3127
3267
|
// closed-set, and idempotent on an already-clean query, so applying them here
|
|
3128
3268
|
// only ever WIDENS what resolves, never narrows it.
|
|
3129
|
-
|
|
3269
|
+
//
|
|
3270
|
+
// stripFillerWords (normalize.mjs) joins the set here (deferred fast-loop
|
|
3271
|
+
// finding, closed out for real): a leading discourse filler that applyPreambleFrames'
|
|
3272
|
+
// own LEADING_CONNECTIVE_RE doesn't catch ("so um, like, what does the store
|
|
3273
|
+
// module do exactly?" — the gate right after "so" requires an ALREADY-interrogative
|
|
3274
|
+
// remainder, which "um, like, what does…" isn't) left MODULE_ORIENT_RE's own
|
|
3275
|
+
// "^what does …" anchor unmatched, so this lane silently declined and the
|
|
3276
|
+
// query fell all the way to the tailored-miss wall. Run AFTER applyPreambleFrames
|
|
3277
|
+
// (same order normalizeQuery's own pipeline uses — preamble frames need their
|
|
3278
|
+
// anchor words, like "so"/"please", intact) and BEFORE the politeness regex
|
|
3279
|
+
// (stripFillerWords already eats "please"/"could you" as filler; the politeness
|
|
3280
|
+
// regex only adds the "explain [to me]" wrapper on top).
|
|
3281
|
+
q = stripFillerWords(applyPreambleFrames(correctMisspellings(q))).replace(MODULE_ORIENT_POLITENESS_RE, "");
|
|
3130
3282
|
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
3131
3283
|
if (!m) return null;
|
|
3132
3284
|
const term = m[1].trim();
|
|
@@ -6573,6 +6725,84 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
6573
6725
|
}
|
|
6574
6726
|
}
|
|
6575
6727
|
|
|
6728
|
+
/** COMPARE (HANDOVER.md 2026-07-12 "no comparison capability" item) — a scoped
|
|
6729
|
+
* v1: "how is X different from Y", "how does X differ from Y", "compare X and
|
|
6730
|
+
* Y"/"compare X with/to Y", "what's the difference between X and Y". Five
|
|
6731
|
+
* closed patterns, same discipline as DESCRIBE_WRAPPER_RE/DETAILED_HOW_WORKS_RE
|
|
6732
|
+
* above — curated anchors, never a general "any two nouns" catch-all. Named
|
|
6733
|
+
* capture groups (a/b) so compareAnswer doesn't need to know which pattern
|
|
6734
|
+
* fired. Tried as a LAST-RESORT rescue (same call-site discipline as (4d)/(4e)
|
|
6735
|
+
* below) since neither ask.mjs's compositional grammar nor any existing lane
|
|
6736
|
+
* recognizes a two-entity comparison at all — there is nothing for this to
|
|
6737
|
+
* shadow. */
|
|
6738
|
+
const COMPARE_PATTERNS = [
|
|
6739
|
+
/^how\s+(?:is|are)\s+(?<a>.+?)\s+different\s+from\s+(?<b>.+?)$/i,
|
|
6740
|
+
/^how\s+do(?:es)?\s+(?<a>.+?)\s+differ\s+from\s+(?<b>.+?)$/i,
|
|
6741
|
+
/^how\s+are\s+(?<a>.+?)\s+and\s+(?<b>.+?)\s+different$/i,
|
|
6742
|
+
/^compare\s+(?<a>.+?)\s+(?:and|with|to)\s+(?<b>.+?)$/i,
|
|
6743
|
+
/^(?:what(?:'s|\s+is)\s+the\s+difference\s+between|difference\s+between)\s+(?<a>.+?)\s+and\s+(?<b>.+?)$/i,
|
|
6744
|
+
];
|
|
6745
|
+
|
|
6746
|
+
/** Strip a leading article — resolveSymbol (codegraph.mjs) has no article
|
|
6747
|
+
* tolerance of its own (same reasoning as describeGrainRescue's own strip,
|
|
6748
|
+
* above): "the TaskController" never resolves where "TaskController" does. */
|
|
6749
|
+
function stripCompareArticle(term) {
|
|
6750
|
+
return String(term || "").trim().replace(/^(?:the|a|an)\s+/i, "").trim();
|
|
6751
|
+
}
|
|
6752
|
+
|
|
6753
|
+
/** Resolves both named entities via resolveSymbol (the SAME resolver
|
|
6754
|
+
* dispatchTool("tmct_describe") uses — no new resolution machinery) and
|
|
6755
|
+
* renders their comparison via renderCompare (codegraph.mjs), which itself
|
|
6756
|
+
* reuses describe's own edgesFor/relLabel/capJoin. Returns null when the
|
|
6757
|
+
* query text doesn't match any COMPARE_PATTERNS shape at all (not this
|
|
6758
|
+
* lane's turn); otherwise always returns a real, honest answer — either the
|
|
6759
|
+
* comparison text or a stated reason it couldn't be done (a term didn't
|
|
6760
|
+
* resolve, or the two resolved to different kinds), never a guess. */
|
|
6761
|
+
// Loads its own graph via loadGraph (server.mjs) when runAsk's own `graph`
|
|
6762
|
+
// param is null (the common case — see runAsk's own `if (graph && …) … else
|
|
6763
|
+
// dispatchTool("tmct_ask", …)` split, above this lane's call site: most
|
|
6764
|
+
// turns never get a preloaded graph threaded in; only dispatchTool's OWN
|
|
6765
|
+
// tools load one, per call, from config). Declines (returns null) on load
|
|
6766
|
+
// failure — a genuinely graph-less repo — the same honest-decline-on-throw
|
|
6767
|
+
// pattern describeGrainRescue/describeWrapperAnswer already use.
|
|
6768
|
+
async function compareAnswer(query, { graph, config, source }) {
|
|
6769
|
+
const q = String(query || "").trim().replace(/\?+$/, "").trim();
|
|
6770
|
+
let m = null;
|
|
6771
|
+
for (const re of COMPARE_PATTERNS) {
|
|
6772
|
+
m = q.match(re);
|
|
6773
|
+
if (m) break;
|
|
6774
|
+
}
|
|
6775
|
+
if (!m) return null;
|
|
6776
|
+
const termA = stripCompareArticle(m.groups?.a);
|
|
6777
|
+
const termB = stripCompareArticle(m.groups?.b);
|
|
6778
|
+
if (!termA || !termB) return null;
|
|
6779
|
+
let g = graph;
|
|
6780
|
+
if (!g) {
|
|
6781
|
+
try {
|
|
6782
|
+
g = await loadGraph(config, source);
|
|
6783
|
+
} catch {
|
|
6784
|
+
return null; // no graph yet — decline, the ordinary wall stands unchanged
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
const { match: indA } = resolveSymbol(g, termA);
|
|
6788
|
+
const { match: indB } = resolveSymbol(g, termB);
|
|
6789
|
+
if (!indA || !indB) {
|
|
6790
|
+
const missing = !indA && !indB ? `"${termA}" and "${termB}" don't` : (!indA ? `"${termA}" doesn't` : `"${termB}" doesn't`);
|
|
6791
|
+
return { text: `I can't compare these — ${missing} resolve to anything in the current artifact.`, ents: [] };
|
|
6792
|
+
}
|
|
6793
|
+
if (indA.id === indB.id) {
|
|
6794
|
+
return { text: `"${indA.label}" and "${indB.label}" resolve to the same entity — nothing to compare.`, ents: [indA] };
|
|
6795
|
+
}
|
|
6796
|
+
const cmp = renderCompare(g, indA, indB);
|
|
6797
|
+
if (!cmp) {
|
|
6798
|
+
return {
|
|
6799
|
+
text: `I can only compare two entities of the SAME kind right now — "${indA.label}" is a ${indA.class || "Entity"} and "${indB.label}" is a ${indB.class || "Entity"}.`,
|
|
6800
|
+
ents: [indA, indB],
|
|
6801
|
+
};
|
|
6802
|
+
}
|
|
6803
|
+
return { text: cmp, ents: [indA, indB] };
|
|
6804
|
+
}
|
|
6805
|
+
|
|
6576
6806
|
/** DETAILED-SUMMARY / EXPLAIN-IN-DETAIL closed phrasings (HANDOVER.md 2026-07-10 item
|
|
6577
6807
|
* 7) — "give me a detailed summary of how the task system works" / "explain in detail
|
|
6578
6808
|
* how X works" / "give me a detailed overview of X". PLAYTESTBENCH_1.4.1.md round 3
|
|
@@ -7209,7 +7439,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7209
7439
|
} catch { /* leave false — the ordinary path decides */ }
|
|
7210
7440
|
}
|
|
7211
7441
|
}
|
|
7212
|
-
const
|
|
7442
|
+
const conversationalCandidateBaseGate = !handled && miss && !envelope?.parsed && !isWhatAboutContinuation && !isDescribePronounContinuation && !isExplainTouch && !isStaccatoNegation && !isVagueRelationTouch && !isStaccatoComparative && !isStaccatoPronounNoFocus;
|
|
7443
|
+
const isConversationalCandidate = conversationalCandidateBaseGate && isConversational(query);
|
|
7213
7444
|
// BUG 2 fix (2026-07-09): "what is X" with NO article ("what is john") is BOTH
|
|
7214
7445
|
// conversational-shaped (≤3 words, no code-ish token — isConversational() would
|
|
7215
7446
|
// claim it) AND a legitimate bare meta/fact-lookup form (BARE_WHATIS_RE —
|
|
@@ -7240,8 +7471,20 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7240
7471
|
// chance to run before the orientation card claims the turn.
|
|
7241
7472
|
const bareWhatisShape = BARE_WHATIS_RE.test(String(query).trim());
|
|
7242
7473
|
const isAdjectiveShape = IS_ADJECTIVE_YESNO_RE.test(String(query).trim());
|
|
7474
|
+
// HANDOVER.md 2026-07-12 CamelCase finding: `isBareCamelCaseMetaQuestion` (see
|
|
7475
|
+
// its own docblock, above isConversational) OR's in alongside
|
|
7476
|
+
// isConversationalCandidate for THIS lane only — a bare "what is TaskController"
|
|
7477
|
+
// (CamelCase compound, no article) is otherwise excluded solely because
|
|
7478
|
+
// isConversational()'s codeish check fires on the CamelCase transition, even
|
|
7479
|
+
// though every other precondition (miss, no structural parse, no continuation
|
|
7480
|
+
// in flight) already holds. Shares the SAME base gate as isConversationalCandidate
|
|
7481
|
+
// (conversationalCandidateBaseGate) so it's never looser. Scoped to this `if`
|
|
7482
|
+
// alone: the `else if (isConversationalCandidate)` orientation-card fallback
|
|
7483
|
+
// further down is UNCHANGED, so a CamelCase term with no real hit still falls
|
|
7484
|
+
// through to its existing miss handling, never the generic orientation card.
|
|
7485
|
+
const isBareCamelCaseWhatisCandidate = conversationalCandidateBaseGate && isBareCamelCaseMetaQuestion(query);
|
|
7243
7486
|
let bareMetaHit = null;
|
|
7244
|
-
if (isConversationalCandidate && (bareWhatisShape || isAdjectiveShape)) {
|
|
7487
|
+
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape)) {
|
|
7245
7488
|
if (memoryDir) {
|
|
7246
7489
|
bareMetaHit = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle))
|
|
7247
7490
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle));
|
|
@@ -7576,6 +7819,32 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7576
7819
|
note(trace, "goal: produce a grounded, cited, multi-sentence account of the subject (not a single fact/definition)");
|
|
7577
7820
|
}
|
|
7578
7821
|
}
|
|
7822
|
+
// (4f) COMPARE RESCUE (HANDOVER.md 2026-07-12 "no comparison capability" item) —
|
|
7823
|
+
// "how is X different from Y" / "compare X and Y" / "what's the difference
|
|
7824
|
+
// between X and Y": resolves BOTH named entities (resolveSymbol, the same
|
|
7825
|
+
// resolver /describe uses) and renders their comparison (renderCompare,
|
|
7826
|
+
// codegraph.mjs — reuses describe's own edgesFor/relLabel/capJoin, no new
|
|
7827
|
+
// graph traversal). Tried ONLY here, after every other lane declined — same
|
|
7828
|
+
// last-resort discipline as (4d)/(4e) above — since no earlier lane (nor
|
|
7829
|
+
// ask.mjs's compositional grammar) recognizes a two-entity comparison at all,
|
|
7830
|
+
// so there is nothing this could shadow. Always a real answer once its
|
|
7831
|
+
// pattern matches: either the comparison, or an honest stated reason it
|
|
7832
|
+
// couldn't be done (a term didn't resolve, or the two are different kinds) —
|
|
7833
|
+
// never a guess, never a forced comparison across mismatched kinds.
|
|
7834
|
+
if (miss && recordMiss && via === "composed") {
|
|
7835
|
+
const compared = await compareAnswer(query, { graph, config, source });
|
|
7836
|
+
if (compared) {
|
|
7837
|
+
answer = compared.text; via = "compare"; recordMiss = false;
|
|
7838
|
+
note(trace, "lane: (4f) COMPARE RESCUE — a \"how is X different from Y\"/\"compare X and Y\" shape matched, answered via renderCompare (codegraph.mjs)");
|
|
7839
|
+
note(trace, "goal: surface the genuine differences between two named entities' facts/edges");
|
|
7840
|
+
if (compared.ents.length) {
|
|
7841
|
+
const last = compared.ents[compared.ents.length - 1];
|
|
7842
|
+
resolvedIds = compared.ents.map((e) => e.id);
|
|
7843
|
+
newFocus = nextFocus(graph, newFocus, last);
|
|
7844
|
+
note(trace, `result: compare resolved "${query}" -> ${compared.ents.map((e) => e.label).join(" vs ")} — the last-named entity becomes the new focus`);
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7579
7848
|
// (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
|
|
7580
7849
|
// wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
|
|
7581
7850
|
// WALL KINDNESS (0.8.2 WS4 (a)): when the PREVIOUS turn's answer was already a
|
|
@@ -8010,7 +8279,34 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null })
|
|
|
8010
8279
|
.map((t) => `${normFactTerm(t.subject)} ${t.predicate} ${normFactTerm(t.object)}`)
|
|
8011
8280
|
.join("; ");
|
|
8012
8281
|
const n = res.ids.length;
|
|
8013
|
-
|
|
8282
|
+
// PLAN_BREADTH_FIRST_NLU.md (c) / ROADMAP.md "Ambition": a paraphrase of
|
|
8283
|
+
// the confirmation sits NEXT TO the literal one, never instead of it, and
|
|
8284
|
+
// only when its accuracy is checked via syllogise.mjs's own transitive-
|
|
8285
|
+
// closure machinery (paraphrase.mjs's verifySubClassParaphrase) against
|
|
8286
|
+
// the SAME pre-existing taught edges — never an unverified paraphrase.
|
|
8287
|
+
// Scoped to the single-triple rdfs:subClassOf shape (the one predicate
|
|
8288
|
+
// family syllogise.mjs's deriveSubClassClosure reasons over); any other
|
|
8289
|
+
// shape (multi-triple sentences, other predicate families) shows only the
|
|
8290
|
+
// original confirmation, unchanged.
|
|
8291
|
+
let paraphraseSuffix = "";
|
|
8292
|
+
if (res.triples.length === 1 && res.triples[0].predicate === SUBCLASS_PREDICATE) {
|
|
8293
|
+
try {
|
|
8294
|
+
const { paraphraseVerifiedSubClass } = await import("./paraphrase.mjs");
|
|
8295
|
+
// Normalized (same normFactTerm cleanup `shown` above already applies)
|
|
8296
|
+
// so the generated paraphrase text reads like "cache is a kind of
|
|
8297
|
+
// component", never a raw lexicon-prefixed form like "tmct:cache".
|
|
8298
|
+
const newSubj = normFactTerm(res.triples[0].subject);
|
|
8299
|
+
const newObj = normFactTerm(res.triples[0].object);
|
|
8300
|
+
const isTaughtRow = (f) => !f.sourceTypes?.includes("corpus") && !f.sourceTypes?.includes("web");
|
|
8301
|
+
const priorEdges = (await factRows(memoryDir))
|
|
8302
|
+
.filter((f) => f.predicate === SUBCLASS_PREDICATE && isTaughtRow(f)
|
|
8303
|
+
&& !(normFactTerm(f.subject) === newSubj && normFactTerm(f.object) === newObj))
|
|
8304
|
+
.map((f) => [normFactTerm(f.subject), normFactTerm(f.object)]);
|
|
8305
|
+
const para = paraphraseVerifiedSubClass(newSubj, newObj, priorEdges);
|
|
8306
|
+
if (para) paraphraseSuffix = ` (${para})`;
|
|
8307
|
+
} catch { /* best-effort — the literal confirmation above is already correct either way */ }
|
|
8308
|
+
}
|
|
8309
|
+
const answer = `noted — remembered ${n} fact${n === 1 ? "" : "s"}: ${shown}${paraphraseSuffix}`;
|
|
8014
8310
|
// PLAN_BREADTH_FIRST_NLU.md §Track 6 (operator directive): the canonical
|
|
8015
8311
|
// restatement of what was committed — `english` reuses the SAME confirmation
|
|
8016
8312
|
// text just shown (already tmct's own preferred subject-predicate-object
|
|
@@ -8282,6 +8578,19 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8282
8578
|
return withLast(plainTurn(workingLine, quantifierRecall, { via: "fact", focus }), "recall a taught quantifier");
|
|
8283
8579
|
}
|
|
8284
8580
|
}
|
|
8581
|
+
// Edge-nominalized "how many X" counts ("how many tests cover Y", "how many
|
|
8582
|
+
// callers does Y have") — checked BEFORE answerCount (same precedence
|
|
8583
|
+
// pattern as answerQuantifierRecall/answerMemoryCount above): answerCount's
|
|
8584
|
+
// own COUNT_NOUNS table doesn't know these nouns at all and would otherwise
|
|
8585
|
+
// short-circuit straight to "I can't count 'tests'" before this lane ever
|
|
8586
|
+
// got a turn. Declines (null) for anything answerCount should own, or a bare
|
|
8587
|
+
// global count with no named entity — see answerEdgeCount's own docblock.
|
|
8588
|
+
const edgeCount = await answerEdgeCount(graph, workingLine);
|
|
8589
|
+
if (edgeCount != null) {
|
|
8590
|
+
note(trace, 'goal: get a per-entity count of an edge-nominalized kind ("how many tests cover X", "how many callers does X have")');
|
|
8591
|
+
note(trace, "lane: answerEdgeCount — matched an EDGE_NOUN_TO_METRIC noun with a resolvable named entity, answered via the same degreeMetric the superlative lane uses");
|
|
8592
|
+
return withLast(plainTurn(workingLine, edgeCount, { via: "count", focus }), "get a per-entity edge count");
|
|
8593
|
+
}
|
|
8285
8594
|
// Aggregate/count questions are answered mechanically off the loaded graph header,
|
|
8286
8595
|
// BEFORE falling through to the ask engine (focus unchanged — a count names no entity).
|
|
8287
8596
|
const count = answerCount(graph, workingLine);
|
package/src/codegraph.mjs
CHANGED
|
@@ -277,6 +277,81 @@ function truncationNote(graph) {
|
|
|
277
277
|
return `note: partial edge lists for: ${list}. Counts are complete; the lists are not.`;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
// ---- compare (scoped v1, HANDOVER.md 2026-07-12 "no comparison capability" item) ----
|
|
281
|
+
|
|
282
|
+
/** One side-by-side row for a single classified relation (predicate/prop pair),
|
|
283
|
+
* in the given direction — reuses edgesFor/relLabel/capJoin verbatim (the SAME
|
|
284
|
+
* classified relation groups and edge-cap discipline renderDescribe reads), just
|
|
285
|
+
* paired up instead of listed independently per entity. `field` picks the
|
|
286
|
+
* correct edge endpoint for the direction (`out` reads the OBJECT end,
|
|
287
|
+
* `incoming` reads the SUBJECT end) — edgesFor's own out/incoming split. */
|
|
288
|
+
function compareRow(prefix, key, aEdges, bEdges, labelA, labelB, field) {
|
|
289
|
+
const fmt = (edges) => (edges.length ? capJoin(edges.map((e) => e[`${field}Label`] || e[field]), DESCRIBE_EDGE_CAP) : "none");
|
|
290
|
+
return ` ${prefix}${key}: ${labelA} (${aEdges.length}) -> ${fmt(aEdges)}; ${labelB} (${bEdges.length}) -> ${fmt(bEdges)}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** predicate-label -> {group, aEdges, bEdges}, built from BOTH sides' edge
|
|
294
|
+
* groups for one direction (out or incoming) — a plain union-by-key merge, no
|
|
295
|
+
* new graph query: every group/edges pair here is exactly what edgesFor already
|
|
296
|
+
* returned for each individual separately. */
|
|
297
|
+
function pairByPredicate(aGroups, bGroups) {
|
|
298
|
+
const byPred = new Map();
|
|
299
|
+
for (const { group, edges } of aGroups) byPred.set(relLabel(group), { group, aEdges: edges, bEdges: [] });
|
|
300
|
+
for (const { group, edges } of bGroups) {
|
|
301
|
+
const key = relLabel(group);
|
|
302
|
+
if (!byPred.has(key)) byPred.set(key, { group, aEdges: [], bEdges: [] });
|
|
303
|
+
byPred.get(key).bEdges = edges;
|
|
304
|
+
}
|
|
305
|
+
return byPred;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Compact, honest side-by-side comparison of two SAME-KIND individuals —
|
|
309
|
+
* the scoped-down v1 comparison capability (HANDOVER.md 2026-07-12): reuses
|
|
310
|
+
* the exact edgesFor/relLabel/capJoin machinery renderDescribe already reads
|
|
311
|
+
* (same classified relation groups, same DESCRIBE_EDGE_CAP discipline), just
|
|
312
|
+
* rendered as a paired diff instead of two independent one-entity reports —
|
|
313
|
+
* no new graph traversal, only a new presentation over data describe already
|
|
314
|
+
* surfaces. Deliberately refuses (returns null) rather than forcing a
|
|
315
|
+
* comparison across mismatched kinds or the same individual twice — the
|
|
316
|
+
* caller (chat.mjs's compare lane) renders its own honest message for those
|
|
317
|
+
* cases instead of an empty/degenerate report. */
|
|
318
|
+
export function renderCompare(graph, indA, indB) {
|
|
319
|
+
if (!indA || !indB || indA.id === indB.id) return null;
|
|
320
|
+
const klass = indA.class || "Entity";
|
|
321
|
+
if ((indB.class || "Entity") !== klass) return null;
|
|
322
|
+
|
|
323
|
+
const lines = [`Comparing ${indA.label} and ${indB.label} (both ${klass}):`];
|
|
324
|
+
const a = edgesFor(graph, indA.id);
|
|
325
|
+
const b = edgesFor(graph, indB.id);
|
|
326
|
+
const outByPred = pairByPredicate(a.out, b.out);
|
|
327
|
+
const inByPred = pairByPredicate(a.incoming, b.incoming);
|
|
328
|
+
|
|
329
|
+
if (!outByPred.size && !inByPred.size) {
|
|
330
|
+
lines.push(" edges: none recorded for either in the current artifact.");
|
|
331
|
+
} else {
|
|
332
|
+
for (const [key, { aEdges, bEdges }] of outByPred) {
|
|
333
|
+
lines.push(compareRow("", key, aEdges, bEdges, indA.label, indB.label, "object"));
|
|
334
|
+
}
|
|
335
|
+
for (const [key, { aEdges, bEdges }] of inByPred) {
|
|
336
|
+
lines.push(compareRow("<- ", key, aEdges, bEdges, indA.label, indB.label, "subject"));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Attribute diff — same key-union approach, but only MISMATCHES are worth
|
|
341
|
+
// surfacing (a shared attribute value isn't a "difference").
|
|
342
|
+
const attrsA = new Map((indA.attributes || []).map((x) => [x.key, x.value]));
|
|
343
|
+
const attrsB = new Map((indB.attributes || []).map((x) => [x.key, x.value]));
|
|
344
|
+
const attrKeys = new Set([...attrsA.keys(), ...attrsB.keys()]);
|
|
345
|
+
for (const k of attrKeys) {
|
|
346
|
+
const va = attrsA.has(k) ? attrsA.get(k) : "(none)";
|
|
347
|
+
const vb = attrsB.has(k) ? attrsB.get(k) : "(none)";
|
|
348
|
+
if (va !== vb) lines.push(` attribute ${k}: ${indA.label} = ${va}; ${indB.label} = ${vb}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (graph.truncated.length) lines.push(truncationNote(graph));
|
|
352
|
+
return lines.join("\n");
|
|
353
|
+
}
|
|
354
|
+
|
|
280
355
|
// ---- impact (transitive reverse closure over imports/calls) ---------------------
|
|
281
356
|
|
|
282
357
|
/**
|
|
@@ -152,6 +152,50 @@ export function correctMisspellings(text) {
|
|
|
152
152
|
return String(text || "").replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/** FILLER_WORDS (ask-vocab.mjs), pre-built into one alternation once at module
|
|
156
|
+
* load — same "build the regex from the table once, reuse it" discipline as
|
|
157
|
+
* every other closed-vocabulary regex in this file (CONTRACTION_RE,
|
|
158
|
+
* MISSPELLING_RE, …), rather than rebuilding it inside stripFillerWords on
|
|
159
|
+
* every call. */
|
|
160
|
+
const FILLER_RE = FILLER_WORDS.length
|
|
161
|
+
? new RegExp(
|
|
162
|
+
"\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b\\s*,?",
|
|
163
|
+
"gi",
|
|
164
|
+
)
|
|
165
|
+
: null;
|
|
166
|
+
|
|
167
|
+
/** Just the filler/politeness-word strip (ask-vocab.mjs's FILLER_WORDS),
|
|
168
|
+
* standalone — the same "one piece of the pipeline, not the whole thing"
|
|
169
|
+
* need correctMisspellings above exists for: a caller with its own
|
|
170
|
+
* closed anchor regex (chat.mjs's moduleOrientLane matches "^what does …
|
|
171
|
+
* do$" itself) wants leading/embedded filler cleared WITHOUT running
|
|
172
|
+
* normalizeQuery's other, more invasive rewrites (contraction expansion,
|
|
173
|
+
* preamble/subordination/conditional frame rewrites) that can restructure
|
|
174
|
+
* the sentence in ways its own shape-matcher never expects — the exact
|
|
175
|
+
* risk correctMisspellings' own docblock describes for the same reason.
|
|
176
|
+
*
|
|
177
|
+
* A comma trailing the filler word/phrase itself (across any whitespace,
|
|
178
|
+
* e.g. "um, like," or "quickly,") is swallowed WITH it — leading
|
|
179
|
+
* conversational filler is routinely comma-spliced onto the real question
|
|
180
|
+
* ("so um, like, what does X do exactly?"), and stripping only the word
|
|
181
|
+
* leaves the comma stranded as parse-corrupting punctuation debris (a
|
|
182
|
+
* leading "," defeats every `^`-anchored template downstream, both parse
|
|
183
|
+
* strategies and lane-local regexes alike) — the same species of
|
|
184
|
+
* leftover-debris bug the preamble frames elsewhere in this file
|
|
185
|
+
* (GREETING_PREAMBLE_RE et al.) already avoid by consuming their own
|
|
186
|
+
* delimiter. The trailing `,?` is safe to make unconditional (unlike the
|
|
187
|
+
* preamble frames, no delimiter-required gate is needed): it only ever
|
|
188
|
+
* fires immediately after a MATCHED filler word, so it can only ever eat a
|
|
189
|
+
* comma that was already glued to filler, never a comma separating real
|
|
190
|
+
* content ("the modules, and the classes" — that comma sits after the
|
|
191
|
+
* content word "modules", not after any filler word). Pure, idempotent;
|
|
192
|
+
* unmatched text passes through byte-unchanged. */
|
|
193
|
+
export function stripFillerWords(text) {
|
|
194
|
+
let q = String(text || "");
|
|
195
|
+
if (FILLER_RE) q = q.replace(FILLER_RE, " ");
|
|
196
|
+
return q.replace(/\s+/g, " ").trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
155
199
|
// ---- closed PREAMBLE frames (0.8.2 feel wave, PLAN_CHAT_FEEL item 2) — the
|
|
156
200
|
// conversational wrapping a developer puts AROUND a real question: a greeting
|
|
157
201
|
// lead-in with a delimiter ("hey there, quick question - …"), a thanks lead-in
|
|
@@ -634,13 +678,7 @@ export function normalizeQuery(text) {
|
|
|
634
678
|
// leaving punctuation/clause debris that poisons the parse.
|
|
635
679
|
q = applySubordinationFrames(q);
|
|
636
680
|
q = applyConditionalFrames(q);
|
|
637
|
-
|
|
638
|
-
const fillerRe = new RegExp(
|
|
639
|
-
"\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
|
|
640
|
-
"gi",
|
|
641
|
-
);
|
|
642
|
-
q = q.replace(fillerRe, " ");
|
|
643
|
-
}
|
|
681
|
+
q = stripFillerWords(q);
|
|
644
682
|
// emphatic trailing punctuation (item 10): a run of terminal "?" collapses to
|
|
645
683
|
// one — the anchored templates consume exactly one optional trailing "?", so
|
|
646
684
|
// "…walk.mjs??" otherwise leaks a stray "?" into the captured object term (the
|