@polycode-projects/the-mechanical-code-talker 3.0.1 → 3.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/README.md +11 -0
- package/bin/tmct.mjs +93 -1
- package/data/templates/constructions/digest-sentence-structures.toml +97 -0
- package/package.json +7 -1
- package/src/adapters/corpus/digest-bank.mjs +62 -0
- package/src/adapters/toml-config.mjs +5 -0
- package/src/domain/ask.mjs +53 -4
- package/src/domain/cli-verbs.mjs +11 -0
- package/src/domain/digest/article.mjs +106 -0
- package/src/domain/digest/compose.mjs +103 -0
- package/src/domain/digest/config.json +13 -0
- package/src/domain/digest/index.mjs +38 -0
- package/src/domain/digest/select.mjs +233 -0
- package/src/domain/digest/store-stats.mjs +77 -0
- package/src/domain/digest/structures.mjs +100 -0
- package/src/domain/digest/words.mjs +40 -0
- package/src/domain/discourse.mjs +0 -0
- package/src/domain/hash.mjs +6 -1
- package/src/domain/router/goal-reasoner.mjs +6 -2
- package/src/services/chat-session.mjs +6 -1
- package/src/services/chat.mjs +317 -53
- package/src/services/ledger-viz.mjs +63 -3
- package/src/services/research-viz.mjs +106 -6
- package/src/surfaces/web/digest-client.mjs +73 -0
- package/src/surfaces/web/ledger-browser-entry.mjs +2 -1
- package/src/surfaces/web/memory-ask-browser-entry.mjs +6 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +144 -139
- package/src/surfaces/web/research-browser-entry.mjs +20 -1
package/src/services/chat.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { dispatchTool, loadGraph, TOOLS } from "../tools/server.mjs";
|
|
|
22
22
|
import { ToolError } from "../adapters/config.mjs";
|
|
23
23
|
import { parseEntities, edgesOfKind, moduleCountOf, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "../domain/codegraph.mjs";
|
|
24
24
|
import { classDisplayName, DYNAMIC_TAIL_OK_RE } from "../domain/ask.mjs";
|
|
25
|
+
import { emptyRecord as emptyDiscourseRecord, advanceTurn as advanceDiscourseTurn, register as registerReferent, bind as bindDiscourseForm } from "../domain/discourse.mjs";
|
|
25
26
|
import { uuidv7 } from "../adapters/uuid.mjs";
|
|
26
27
|
import * as defaultSource from "../adapters/source.mjs";
|
|
27
28
|
import { loadTemplates, render as renderTemplate } from "../adapters/corpus/templates.mjs";
|
|
@@ -1074,6 +1075,15 @@ const CAPABILITY_PHRASES = [
|
|
|
1074
1075
|
/^(?:can you\s+)?walk me through (?:this|the)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
1075
1076
|
/^(?:what(?:'s|s|\s+is)|give me|show me|gimme) the big picture(?:\s+(?:here|(?:on|of|for|about)\s+(?:this|the)\s+(?:app|codebase|repo|repository|project|code)))?\??$/i,
|
|
1076
1077
|
/^(?:give me|what's) the lay of the land\??$/i,
|
|
1078
|
+
// "give me an overview" / "an overview" — the plain-word sibling of "the
|
|
1079
|
+
// big picture" just above, same optional here/of-this-repo tail. Without a
|
|
1080
|
+
// closed entry the word "overview" prose-matches real symbols in an
|
|
1081
|
+
// indexed graph (moduleOverviewText) and the describe rescue dumps that
|
|
1082
|
+
// symbol's card. The detailed forms ("give me a detailed overview of X")
|
|
1083
|
+
// carry a mandatory "detailed"+of-term and stay with the completions
|
|
1084
|
+
// rescue, untouched by this anchor.
|
|
1085
|
+
/^(?:(?:can|could|would) you\s+)?(?:give me|show me|gimme)\s+an overview(?:\s+(?:here|(?:on|of|for|about)\s+(?:this|the)\s+(?:app|codebase|repo|repository|project|code)))?\??$/i,
|
|
1086
|
+
/^an overview(?:\s+please)?\??$/i,
|
|
1077
1087
|
// "what have we got here"/"what've we got here" — a casual, self-answering
|
|
1078
1088
|
// opener (matches after a leading "so" strips via LEADING_CONNECTIVE_RE,
|
|
1079
1089
|
// leaving this as the bare remainder).
|
|
@@ -5545,9 +5555,24 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
5545
5555
|
// to end in a module path would be claimed here.
|
|
5546
5556
|
const tailLooksLikePath = !!(m || identityMatch) && phraseWords.length > 1 && MODULE_PATH_RE.test(pathTail);
|
|
5547
5557
|
// The identity phrasing ("what is <term>") only ever claims a path-shaped
|
|
5548
|
-
// term — bare, or with modifier words ahead of a path-shaped tail
|
|
5549
|
-
//
|
|
5550
|
-
|
|
5558
|
+
// term — bare, or with modifier words ahead of a path-shaped tail — or,
|
|
5559
|
+
// below, a bare extensionless module basename.
|
|
5560
|
+
//
|
|
5561
|
+
// "what is codegraph": both siblings of that question already resolve the
|
|
5562
|
+
// module ("what is codegraph.mjs" via MODULE_PATH_RE, "describe codegraph"
|
|
5563
|
+
// via resolveSymbol's basename tier), so the extensionless identity form
|
|
5564
|
+
// resolves by the same evidence — exact basename-stem equality against
|
|
5565
|
+
// exactly ONE module. The gate stays strict: a single bare word with no
|
|
5566
|
+
// article (an articled "what is a dog" keeps its vocabulary reading), and
|
|
5567
|
+
// any tie or non-module term declines unchanged.
|
|
5568
|
+
if (!m && !MODULE_PATH_RE.test(bare) && !tailLooksLikePath) {
|
|
5569
|
+
if (!identityMatch || !/^[\w$][\w$.-]*$/.test(identityMatch)) return null;
|
|
5570
|
+
const stemLc = bare.toLowerCase();
|
|
5571
|
+
const stemHits = graph.individuals.filter((i) => i.class === "Module"
|
|
5572
|
+
&& String(i.label).toLowerCase().split("/").pop().replace(/\.[a-z0-9]+$/, "") === stemLc);
|
|
5573
|
+
if (stemHits.length !== 1) return null;
|
|
5574
|
+
return { text: moduleOverviewText(graph, stemHits[0]), via: "meta" };
|
|
5575
|
+
}
|
|
5551
5576
|
const ent = await resolveEntity(graph, m ? phrase : bare);
|
|
5552
5577
|
if (ent) {
|
|
5553
5578
|
const ind = graph.byId?.get?.(ent.id);
|
|
@@ -5921,6 +5946,10 @@ async function presuppositionNudge(query, { graph, memoryDir }) {
|
|
|
5921
5946
|
* WALL_MISS_RE: the suppression keys on the PREVIOUS answer matching it, so this
|
|
5922
5947
|
* text self-limits — a third consecutive miss re-offers the tailored hint. */
|
|
5923
5948
|
const WALL_REPEAT_ONELINER = "still couldn't parse that — /help lists every query shape.";
|
|
5949
|
+
/** The graph-less bootstrap wall's opening line — shared with the teach-offer
|
|
5950
|
+
* collapse below, which treats this wall (like the shortened generic wall)
|
|
5951
|
+
* as text a term-specific offer REPLACES rather than stacks under. */
|
|
5952
|
+
const NO_GRAPH_BOOTSTRAP_WALL_LEAD = "I can't answer that as a code question — no code graph is loaded in this session.";
|
|
5924
5953
|
|
|
5925
5954
|
/** The orientation-repeat one-liner. The conversational
|
|
5926
5955
|
* orientation branch sits OUTSIDE the composed-only wall-shortening gate (it
|
|
@@ -6411,6 +6440,37 @@ function senseSplitFactList(hits, rows, subjectVariants, { indent = "" } = {}) {
|
|
|
6411
6440
|
return { lines, grouped };
|
|
6412
6441
|
}
|
|
6413
6442
|
|
|
6443
|
+
// A subject-scan term answer longer than this many flat fact lines leads with a
|
|
6444
|
+
// deterministic digest paragraph (src/domain/digest) and holds the full list
|
|
6445
|
+
// behind the escape; a shorter answer is already readable as a list.
|
|
6446
|
+
const DIGEST_READBACK_THRESHOLD = 8;
|
|
6447
|
+
|
|
6448
|
+
/** The digest lead for a subject-scan term answer: a bounded narrative first
|
|
6449
|
+
* (selection, sentence structures, composition — all deterministic, no model),
|
|
6450
|
+
* the full fact list held behind the "show the facts"/"more" escape. `termRows`
|
|
6451
|
+
* are the term's own fact rows, `allRows` the whole store the statistics scan
|
|
6452
|
+
* over, `lines` the already-rendered flat fact list the escape reveals.
|
|
6453
|
+
*
|
|
6454
|
+
* Returns { text, pending } or null. Null when the structure bank is
|
|
6455
|
+
* unavailable (the in-browser dock stubs the filesystem loader out) or the
|
|
6456
|
+
* selector kept nothing renderable, so the caller falls back to the flat list —
|
|
6457
|
+
* the same graceful degradation the construction banks take in a browser
|
|
6458
|
+
* bundle. Deterministic; the digest reads only stored facts. */
|
|
6459
|
+
async function termDigestReadBack(term, termRows, allRows, lines) {
|
|
6460
|
+
let digestTermFromRows;
|
|
6461
|
+
try { ({ digestTermFromRows } = await import("../adapters/corpus/digest-bank.mjs")); }
|
|
6462
|
+
catch { return null; }
|
|
6463
|
+
let article;
|
|
6464
|
+
try { article = digestTermFromRows(term, termRows, allRows); }
|
|
6465
|
+
catch { return null; }
|
|
6466
|
+
if (!article || !article.paragraphs.length) return null;
|
|
6467
|
+
const sources = [...new Set((article.sources || []).map((s) => s.provenance).filter(Boolean))];
|
|
6468
|
+
const sourceLine = sources.length ? `(sources: ${sources.join("; ")})\n` : "";
|
|
6469
|
+
const escape = `Say 'show the facts' for all ${lines.length} stored facts.`;
|
|
6470
|
+
const text = `${article.paragraphs.join("\n\n")}\n\n${sourceLine}${escape}`;
|
|
6471
|
+
return { text, pending: { items: lines, noun: "facts" } };
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6414
6474
|
/** "a"/"an" for a term, through the SAME grammar-rules.toml "article" rule and
|
|
6415
6475
|
* finish.mjs's beginsWithVowelSound every other agreement site in this file
|
|
6416
6476
|
* uses — never a hardcoded "a", which is ungrammatical for a vowel-initial
|
|
@@ -7274,8 +7334,8 @@ function withDeducedGoal(res, envelope, query) {
|
|
|
7274
7334
|
}
|
|
7275
7335
|
|
|
7276
7336
|
async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null, focusLabel = null) {
|
|
7277
|
-
let normFactTerm;
|
|
7278
|
-
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
7337
|
+
let normFactTerm; let normFactPredicate;
|
|
7338
|
+
try { ({ normFactTerm, normFactPredicate } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
7279
7339
|
const q = String(query).trim();
|
|
7280
7340
|
|
|
7281
7341
|
// (a-pre) "what is used for riding" / "what can be used for riding" / "what
|
|
@@ -7497,7 +7557,29 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7497
7557
|
// matched against fact subjects, and — the actual bug — the result is
|
|
7498
7558
|
// FILTERED to just that one predicate (mgx:usedFor) instead of every
|
|
7499
7559
|
// relation about the subject undifferentiated.
|
|
7500
|
-
const
|
|
7560
|
+
const split = splitMetaPredicate(metaTerm);
|
|
7561
|
+
const { predicate } = split;
|
|
7562
|
+
// A leading article survives the T5/BARE_WHATIS capture ("what is the car
|
|
7563
|
+
// used for" → "the car") — stripped the same way normFactTerm strips it,
|
|
7564
|
+
// so the article never decides whether the subject matches.
|
|
7565
|
+
let subject = split.subject.replace(/^(?:the|an?)\s+/i, "").trim() || split.subject;
|
|
7566
|
+
// A predicate-shaped ask whose subject is the session anaphor ("what is
|
|
7567
|
+
// it used for") resolves against the standing focus, exactly as the
|
|
7568
|
+
// IS_ADJECTIVE/ISA yes/no readers resolve theirs. With no focus standing
|
|
7569
|
+
// the pronoun is named and declined (the cold-pronoun voice) — never a
|
|
7570
|
+
// fact lookup on the literal word "it", and never a teach-offer for it.
|
|
7571
|
+
let focusSubstituted = false;
|
|
7572
|
+
if (predicate && IS_ADJECTIVE_PRONOUN_RE.test(subject)) {
|
|
7573
|
+
if (!focusLabel) {
|
|
7574
|
+
const tail = String(FACT_PREDICATE_PHRASES[predicate] || "").replace(/^(?:is|are)\s+/, "");
|
|
7575
|
+
return {
|
|
7576
|
+
text: `not sure what "${subject.toLowerCase()}" refers to yet — name the subject directly, e.g. "what is a <name>${tail ? ` ${tail}` : ""}".`,
|
|
7577
|
+
replace: miss, miss: true, selfContainedMiss: true,
|
|
7578
|
+
};
|
|
7579
|
+
}
|
|
7580
|
+
subject = focusLabel;
|
|
7581
|
+
focusSubstituted = true;
|
|
7582
|
+
}
|
|
7501
7583
|
const variants = factTermVariants(normFactTerm, subject);
|
|
7502
7584
|
// factRows (trust+sourceIds-bearing), not the plain memoryFacts shape — the
|
|
7503
7585
|
// bias-weighted ranking below needs each hit's sourceIds to resolve which
|
|
@@ -7506,16 +7588,23 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7506
7588
|
// back where it's hidden or that it's the objective (WORLD_INTERNAL_PREDICATES).
|
|
7507
7589
|
const subjectHits = (await factRows(memoryDir, cache))
|
|
7508
7590
|
.filter((f) => variants.has(f.subject) && !WORLD_INTERNAL_PREDICATES.has(f.predicate));
|
|
7509
|
-
|
|
7591
|
+
// Matched through normFactPredicate, so a fact stored under a minted
|
|
7592
|
+
// spelling of the same relation ("mgx:used-for", from the participle
|
|
7593
|
+
// teach frame, in a store written before the spellings converged) is
|
|
7594
|
+
// found by the curated spelling it means.
|
|
7595
|
+
let hits = predicate ? subjectHits.filter((f) => normFactPredicate(f.predicate) === predicate) : subjectHits;
|
|
7510
7596
|
if (!hits.length) {
|
|
7511
|
-
// The subject itself is known
|
|
7512
|
-
//
|
|
7513
|
-
//
|
|
7514
|
-
//
|
|
7515
|
-
|
|
7597
|
+
// The subject itself is known — as a fact subject, or as the standing
|
|
7598
|
+
// focus a pronoun just resolved to — but not under this specific
|
|
7599
|
+
// relation: an honest, specific "no" rather than falling through to
|
|
7600
|
+
// the generic "isn't a term in this graph's own vocabulary" wall
|
|
7601
|
+
// (which would be actively misleading here: the subject IS a known
|
|
7602
|
+
// term).
|
|
7603
|
+
if (predicate && (subjectHits.length || focusSubstituted)) {
|
|
7516
7604
|
return {
|
|
7517
7605
|
text: `I don't have any "${FACT_PREDICATE_PHRASES[predicate]}" facts about ${subject}.`,
|
|
7518
7606
|
replace: miss,
|
|
7607
|
+
...(subjectHits.length ? {} : { miss: true }),
|
|
7519
7608
|
};
|
|
7520
7609
|
}
|
|
7521
7610
|
// The term names nothing as a fact SUBJECT, but may exist only as the
|
|
@@ -7538,7 +7627,17 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7538
7627
|
// "disclosed, never dropped" contract). Unconfigured/tied bias degrades to
|
|
7539
7628
|
// trust-desc, byte-identical to before this feature existed.
|
|
7540
7629
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7541
|
-
const
|
|
7630
|
+
const allRows = await factRows(memoryDir, cache);
|
|
7631
|
+
const { lines, grouped } = senseSplitFactList(hits, allRows, variants);
|
|
7632
|
+
// A long undifferentiated "what is X" leads with the digest — a bounded
|
|
7633
|
+
// narrative over the same facts — and holds the full list behind the escape.
|
|
7634
|
+
// It wins over the sense-split grouping here: the digest's own selector
|
|
7635
|
+
// filters the mis-sensed branch that grouping would otherwise surface as its
|
|
7636
|
+
// own block. Falls back to grouping/flat when the digest is unavailable.
|
|
7637
|
+
const digested = (!predicate && lines.length > DIGEST_READBACK_THRESHOLD)
|
|
7638
|
+
? await termDigestReadBack(subject, hits, allRows, lines)
|
|
7639
|
+
: null;
|
|
7640
|
+
if (digested) return { ...digested, replace: miss };
|
|
7542
7641
|
if (grouped) return { ...grouped, replace: miss };
|
|
7543
7642
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7544
7643
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
@@ -8355,6 +8454,14 @@ const HAS_METHOD_OPEN_RE = /^what\s+methods\s+does\s+([\w'-]+)\s+have[?.!\s]*$/i
|
|
|
8355
8454
|
* cascade/orientation nudge that already handles it. */
|
|
8356
8455
|
const IS_ADJECTIVE_YESNO_RE = /^(?:is|are|was|were)\s+(.+?)\s+([A-Za-z][\w-]*)[?.!\s]*$/i;
|
|
8357
8456
|
const IS_ADJECTIVE_PRONOUN_RE = /^(?:it|this|that)$/i;
|
|
8457
|
+
/** A backtracked subject that is really a cross-turn temporal comparison —
|
|
8458
|
+
* a bindable form followed by a comparison word ("that before chat.mjs
|
|
8459
|
+
* was", from "was that before chat.mjs was touched"). The comparison lane
|
|
8460
|
+
* owns the closed-participle family; a cousin with a participle outside
|
|
8461
|
+
* that set still lands here, and offering to teach a fact about "that
|
|
8462
|
+
* before chat.mjs was" is a category error, so the property readers
|
|
8463
|
+
* decline it the way they decline a personal-pronoun subject. */
|
|
8464
|
+
const BINDABLE_COMPARISON_SUBJECT_RE = /^(?:it|this|that)(?:\s+one)?\s+(?:before|after)\b/i;
|
|
8358
8465
|
/** IS_ADJECTIVE_YESNO_RE's
|
|
8359
8466
|
* subject capture is unbounded/unrestricted (see its own docblock above), so
|
|
8360
8467
|
* a pronoun-subject IDENTITY question ("are you happy", "are you like
|
|
@@ -8571,6 +8678,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
8571
8678
|
// "you", so `subject` itself would already carry the pronoun verbatim).
|
|
8572
8679
|
if (subject && !/^there\b/i.test(subject) && !envelope?.parsed
|
|
8573
8680
|
&& !IS_ADJECTIVE_YESNO_PRONOUN_SUBJECT_RE.test(rawSubject)
|
|
8681
|
+
&& !BINDABLE_COMPARISON_SUBJECT_RE.test(rawSubject)
|
|
8574
8682
|
&& !PLACE_ADVERB_OBJECT_RE.test(emptyIsAdj[2].trim())) {
|
|
8575
8683
|
return unknownAdjectiveOffer(subject, emptyIsAdj[2].trim().toLowerCase());
|
|
8576
8684
|
}
|
|
@@ -9631,7 +9739,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
9631
9739
|
// this whole reader decline HONESTLY — no fact lookup, no teach-offer —
|
|
9632
9740
|
// and fall through to whatever handles identity/small-talk questions
|
|
9633
9741
|
// instead, rather than special-casing a return here.
|
|
9634
|
-
const subject = IS_ADJECTIVE_YESNO_PRONOUN_SUBJECT_RE.test(rawSubject) ? null
|
|
9742
|
+
const subject = IS_ADJECTIVE_YESNO_PRONOUN_SUBJECT_RE.test(rawSubject) || BINDABLE_COMPARISON_SUBJECT_RE.test(rawSubject) ? null
|
|
9635
9743
|
: IS_ADJECTIVE_PRONOUN_RE.test(rawSubject) ? (focusLabel || null) : rawSubject;
|
|
9636
9744
|
const adjective = isAdj[2].trim().toLowerCase();
|
|
9637
9745
|
if (subject) {
|
|
@@ -11813,7 +11921,31 @@ const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s
|
|
|
11813
11921
|
* than silently accepted alongside the current location. */
|
|
11814
11922
|
const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
|
|
11815
11923
|
|
|
11816
|
-
|
|
11924
|
+
/** "was that before logger.mjs was touched" — a singular bindable form, a
|
|
11925
|
+
* comparison word, and an embedded passive clause. The closed participle set
|
|
11926
|
+
* is the touch family the when-question path answers; anything else keeps
|
|
11927
|
+
* the honest miss. */
|
|
11928
|
+
const TEMPORAL_COMPARISON_RE = /^(?:was|is)\s+(this one|that one|it|this|that)\s+(before|after)\s+(.+?)\s+(?:was|were)\s+(touched|changed|modified|edited|updated)[?.!\s]*$/i;
|
|
11929
|
+
|
|
11930
|
+
/** ARCHITECTURE-OVERVIEW intent — "show me the architecture", "what is the
|
|
11931
|
+
* architecture of this repo": the whole-repo map the /arch command renders.
|
|
11932
|
+
* A closed phrase set, because the literal word "architecture" is also a
|
|
11933
|
+
* plausible SYMBOL substring in many graphs (renderArchitecture,
|
|
11934
|
+
* tmct_architecture) — the symbol-describe rescues would otherwise resolve
|
|
11935
|
+
* the word to one such symbol and dump its definition card instead of the
|
|
11936
|
+
* map. Every phrasing here names the architecture as a TOPIC (an article,
|
|
11937
|
+
* an of-this-repo tail, or an overview/map noun); a query that NAMES a
|
|
11938
|
+
* symbol ("describe renderArchitecture") never matches. */
|
|
11939
|
+
const ARCH_OVERVIEW_LEAD = "(?:(?:can|could|would)\\s+you\\s+(?:please\\s+)?)?(?:(?:show|give)\\s+(?:me|us)\\s+|describe\\s+|explain\\s+|what(?:'s|s|\\s+is)\\s+)?";
|
|
11940
|
+
const ARCH_OVERVIEW_TAIL = "(?:\\s+(?:of|for)\\s+(?:this|the)\\s+(?:app|codebase|repo|repository|project|code))?";
|
|
11941
|
+
const ARCH_OVERVIEW_PHRASES = [
|
|
11942
|
+
// Article-carried: "the architecture" alone, or wrapped/tailed.
|
|
11943
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}the\\s+architecture(?:\\s+(?:overview|map|diagram))?${ARCH_OVERVIEW_TAIL}(?:\\s+here)?\\??$`, "i"),
|
|
11944
|
+
// Article-less: anchored by the of-this-repo tail or the overview/map noun instead.
|
|
11945
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}architecture\\s+(?:(?:of|for)\\s+(?:this|the)\\s+(?:app|codebase|repo|repository|project|code)|overview|map|diagram)\\??$`, "i"),
|
|
11946
|
+
];
|
|
11947
|
+
|
|
11948
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null, discourseHolder = null, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
11817
11949
|
const ts = new Date().toISOString();
|
|
11818
11950
|
// The surface this turn runs on ("cli" default; "browser" from a web entry) —
|
|
11819
11951
|
// the honest-miss tail below points a browser/adventure miss at the teach
|
|
@@ -11842,6 +11974,61 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11842
11974
|
.replace(/^(?:and|so|then|also)\s+/i, "")
|
|
11843
11975
|
.replace(/how many\s+/i, "how many of those ");
|
|
11844
11976
|
}
|
|
11977
|
+
// TEMPORAL COMPARISON ACROSS TURNS — "was that before logger.mjs was
|
|
11978
|
+
// touched": a singular bindable form, a comparison word, and an embedded
|
|
11979
|
+
// passive clause. The form binds against the session's discourse record (a
|
|
11980
|
+
// dated referent a previous answer established), the embedded clause runs
|
|
11981
|
+
// fresh through the same when-question path a standalone turn takes, and
|
|
11982
|
+
// the two ISO dates compare with both sides cited. Checked BEFORE the ask
|
|
11983
|
+
// engine (the same precedence RENAME_HISTORY_RE takes, below) so the
|
|
11984
|
+
// sentence never reaches the keyword-spot strategy's multi-token patient
|
|
11985
|
+
// guard. A form this shape that CANNOT compose still ends here, with a
|
|
11986
|
+
// specific miss naming what's missing (no referent for the form, an
|
|
11987
|
+
// undated referent, no graph, an undatable clause) — falling through used
|
|
11988
|
+
// to hand the sentence to the teach-offer cascade, which read "that before
|
|
11989
|
+
// X was" as a subject to learn facts about.
|
|
11990
|
+
{
|
|
11991
|
+
const cmp = String(query).trim().match(TEMPORAL_COMPARISON_RE);
|
|
11992
|
+
if (cmp) {
|
|
11993
|
+
const [, form, cmpOp, clauseSubject, participle] = cmp;
|
|
11994
|
+
const verb = participle.toLowerCase();
|
|
11995
|
+
const refMiss = (text) => {
|
|
11996
|
+
note(trace, "goal: compare a prior answer's dated referent against a freshly read event (cross-turn temporal composition)");
|
|
11997
|
+
note(trace, `lane: TEMPORAL_COMPARISON_RE — "${form}" could not compose a comparison; a specific miss names why, never the teach-offer cascade`);
|
|
11998
|
+
return plainTurn(query, text, { via: "miss", miss: true, focus });
|
|
11999
|
+
};
|
|
12000
|
+
const bound = discourseHolder ? bindDiscourseForm(discourseHolder.record, form) : null;
|
|
12001
|
+
if (!bound?.referent) {
|
|
12002
|
+
return refMiss(`I don't have a referent for "${form}" yet — nothing answered earlier in this conversation binds it. Ask about the event first (e.g. "when was ${clauseSubject} last ${verb}"), then ask the comparison again.`);
|
|
12003
|
+
}
|
|
12004
|
+
const refDay = String(bound.referent.attrs?.date || "").slice(0, 10);
|
|
12005
|
+
if (!refDay) {
|
|
12006
|
+
return refMiss(`"${form}" refers to ${bound.referent.label}, but I have no date on record for it — so I can't place it before or after ${clauseSubject} was ${verb}.`);
|
|
12007
|
+
}
|
|
12008
|
+
if (!graph) {
|
|
12009
|
+
return refMiss(`"${form}" refers to ${bound.referent.label} (${refDay}), but I need a code graph to date when ${clauseSubject} was last ${verb} — no code graph is loaded.`);
|
|
12010
|
+
}
|
|
12011
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
12012
|
+
const fresh = ask(graph, `when was ${clauseSubject} ${participle}`);
|
|
12013
|
+
const freshHit = (!fresh?.tmct_ask?.miss && !fresh?.tmct_ask?.ambiguous) ? fresh?.tmct_ask?.matches?.[0] : null;
|
|
12014
|
+
const freshCommit = freshHit?.id ? graph.byId?.get?.(freshHit.id) : null;
|
|
12015
|
+
const clauseDay = freshCommit?.class === "Commit"
|
|
12016
|
+
? String((freshCommit.attributes || []).find((a) => a.key === "date")?.value || "").slice(0, 10)
|
|
12017
|
+
: "";
|
|
12018
|
+
if (!clauseDay) {
|
|
12019
|
+
return refMiss(`"${form}" refers to ${bound.referent.label} (${refDay}), but I couldn't date when ${clauseSubject} was last ${verb} in this index — so I can't compare the two.`);
|
|
12020
|
+
}
|
|
12021
|
+
const holds = cmpOp.toLowerCase() === "before" ? refDay < clauseDay : refDay > clauseDay;
|
|
12022
|
+
const relation = refDay < clauseDay ? "came before" : refDay > clauseDay ? "came after" : "landed on the same day as";
|
|
12023
|
+
const text = `${holds ? "Yes" : "No"} — ${bound.referent.label} (${refDay}) ${relation} ${clauseSubject} was last ${verb} (${freshCommit.label}, ${clauseDay}).`;
|
|
12024
|
+
note(trace, "goal: compare a prior answer's dated referent against a freshly read event (cross-turn temporal composition)");
|
|
12025
|
+
note(trace, `lane: TEMPORAL_COMPARISON_RE — "${form}" bound ${bound.referent.label} (${refDay}) through the discourse record; the embedded clause re-ran as its own when-question`);
|
|
12026
|
+
const turn = plainTurn(query, text, { via: "composed", miss: false, focus });
|
|
12027
|
+
const cited = [graph.byId?.get?.(bound.referent.ids[0]), freshCommit].filter(Boolean);
|
|
12028
|
+
turn.detail = { traversal: `discourse ${bound.referent.ref} (${refDay}) vs last-${verb} of ${clauseSubject} (${clauseDay})`, matches: cited };
|
|
12029
|
+
return turn;
|
|
12030
|
+
}
|
|
12031
|
+
}
|
|
11845
12032
|
// RENAME HISTORY — "what was X called before" and its siblings. The index
|
|
11846
12033
|
// records current names only, and without this gate "called" fuzzes onto
|
|
11847
12034
|
// the calls relation ("before" simply drops), so the reply read as fluent
|
|
@@ -12020,6 +12207,17 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12020
12207
|
"isn't resolved to anything yet — name the term directly, or ask a question that resolves one first.",
|
|
12021
12208
|
);
|
|
12022
12209
|
if (envJson) { try { envelope = JSON.parse(envJson); } catch { envelope = null; } }
|
|
12210
|
+
// Typed discourse referents the answer established (the ask envelope's
|
|
12211
|
+
// additive `discourse` field, emitted beside the eval where the answer's
|
|
12212
|
+
// content is still typed) register into the session's record here — the
|
|
12213
|
+
// one point both ask paths (direct call and dispatchTool) converge.
|
|
12214
|
+
if (discourseHolder && Array.isArray(envelope?.discourse)) {
|
|
12215
|
+
for (const { lane, ...spec } of envelope.discourse) {
|
|
12216
|
+
discourseHolder.record = registerReferent(discourseHolder.record, {
|
|
12217
|
+
...spec, from: { turn: discourseHolder.record.turn, lane, query: askQuery },
|
|
12218
|
+
});
|
|
12219
|
+
}
|
|
12220
|
+
}
|
|
12023
12221
|
} catch (e) {
|
|
12024
12222
|
const thrown = String(e?.message || e);
|
|
12025
12223
|
// A graph-less session's ask dispatch fails reading the never-configured
|
|
@@ -12042,7 +12240,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12042
12240
|
answer = (!graph || noCodeGraph(graph)) && (!config || e?.emptyGraph || /^cannot read graph artifact\b/.test(thrown))
|
|
12043
12241
|
// A browser session has no `tmct init in a repo` to reach for, so its
|
|
12044
12242
|
// fallback drops that CLI-only remedy and keeps just the teach pointer.
|
|
12045
|
-
?
|
|
12243
|
+
? `${NO_GRAPH_BOOTSTRAP_WALL_LEAD} ${vocabHint
|
|
12046
12244
|
|| (browser
|
|
12047
12245
|
? "I can still remember and answer taught facts (try \"every bug is an issue\")."
|
|
12048
12246
|
: "I can still remember and answer taught facts (try \"every bug is an issue\"), or run `tmct init` in a repo to index one.")}`
|
|
@@ -12198,10 +12396,32 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12198
12396
|
note(trace, `goal: ${deduced} (revised — the raw \"what else\" phrasing was recognized directly, not the relaxed/reparsed envelope)`);
|
|
12199
12397
|
}
|
|
12200
12398
|
}
|
|
12399
|
+
// (0a) ARCHITECTURE OVERVIEW — see ARCH_OVERVIEW_PHRASES. Answered here,
|
|
12400
|
+
// before the meta/orientation lanes and long before the symbol-resolve
|
|
12401
|
+
// rescues (4d), so the closed architecture phrasings reach the map instead
|
|
12402
|
+
// of a literal-token symbol card or the vocabulary-touch teach offer.
|
|
12403
|
+
if (!handled && miss && graph && !noCodeGraph(graph)) {
|
|
12404
|
+
// The RAW text is tried alongside the peeled one: applyPreambleFrames'
|
|
12405
|
+
// show/give-me bridge rewrites "show me the architecture" into "describe
|
|
12406
|
+
// architecture", which drops the article this closed set anchors on.
|
|
12407
|
+
const archRaw = correctMisspellings(String(query).trim());
|
|
12408
|
+
const archPeeled = applyPreambleFrames(archRaw);
|
|
12409
|
+
if (ARCH_OVERVIEW_PHRASES.some((re) => re.test(archRaw) || re.test(archPeeled))) {
|
|
12410
|
+
try {
|
|
12411
|
+
const archText = await dispatchTool("tmct_architecture", {}, { config, source, tel });
|
|
12412
|
+
if (archText) {
|
|
12413
|
+
answer = archText; via = "meta"; recordMiss = false; handled = true;
|
|
12414
|
+
deduced = "understand the overall architecture (package/module boundaries)";
|
|
12415
|
+
note(trace, `goal: ${deduced} (revised — a closed architecture-overview phrasing was recognized directly)`);
|
|
12416
|
+
note(trace, "lane: (0a) ARCHITECTURE OVERVIEW — routed to the whole-repo architecture map (/arch), never a literal symbol lookup on the word \"architecture\"");
|
|
12417
|
+
}
|
|
12418
|
+
} catch { /* the tool couldn't load a graph — the ordinary lanes decide */ }
|
|
12419
|
+
}
|
|
12420
|
+
}
|
|
12201
12421
|
// (1) #2 META/SELF: bare self/session questions ("what do you know", "what is this
|
|
12202
12422
|
// codebase", "how do i start") → a summary / orientation, answered before the
|
|
12203
12423
|
// fact-dump readers so "what do you know" gets a summary, not raw facts.
|
|
12204
|
-
if (miss) {
|
|
12424
|
+
if (!handled && miss) {
|
|
12205
12425
|
const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint, focus });
|
|
12206
12426
|
if (meta) {
|
|
12207
12427
|
// A lane may answer with a better-worded decline (the module-orient
|
|
@@ -12509,6 +12729,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12509
12729
|
if (fallback) bareMetaHit = { text: fallback.text, replace: true };
|
|
12510
12730
|
}
|
|
12511
12731
|
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
12732
|
+
let selfContainedMiss = false;
|
|
12512
12733
|
if (bareMetaHit?.reference) {
|
|
12513
12734
|
// The bare-form reference hit mirrors (4h): the cited answer replaces the
|
|
12514
12735
|
// miss, the turn is no longer recorded as one, and the article's grounded
|
|
@@ -12632,6 +12853,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12632
12853
|
// (the isa ladder's "I can't confirm that" closers) — the turn record
|
|
12633
12854
|
// keeps miss=true and via stays untouched, so miss-rate metrics and
|
|
12634
12855
|
// recall's own miss-gated lanes see it exactly like the wall it replaced.
|
|
12856
|
+
// One flagged `selfContainedMiss` already names its own recovery, so
|
|
12857
|
+
// the empty-graph orientation pointer below stays off it — a pronoun
|
|
12858
|
+
// decline with an index pointer under it is two answers to one turn.
|
|
12859
|
+
if (fact.selfContainedMiss) selfContainedMiss = true;
|
|
12635
12860
|
if (!fact.miss) {
|
|
12636
12861
|
via = "fact";
|
|
12637
12862
|
recordMiss = false;
|
|
@@ -12980,6 +13205,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12980
13205
|
// WALL KINDNESS: a second consecutive wall collapses to a one-liner whose
|
|
12981
13206
|
// text does NOT match WALL_MISS_RE — self-limiting, so a third consecutive
|
|
12982
13207
|
// miss re-offers the tailored hint instead of droning.
|
|
13208
|
+
let genericWallMiss = false;
|
|
12983
13209
|
if (miss && recordMiss && via === "composed" && WALL_MISS_RE.test(answer)) {
|
|
12984
13210
|
const repeat = last?.answer && WALL_MISS_RE.test(String(last.answer));
|
|
12985
13211
|
// A GRAPH-LESS session's wall must not hand a vocabulary question a list
|
|
@@ -12990,33 +13216,17 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12990
13216
|
? `I couldn't read that as a question I can answer. ${vocabHint} Type /help for all query shapes.`
|
|
12991
13217
|
: shortMissHint(query));
|
|
12992
13218
|
via = "miss";
|
|
13219
|
+
genericWallMiss = true;
|
|
12993
13220
|
note(trace, `lane: (5) SHORT TAILORED MISS — every lane above declined; ${repeat ? "REPEAT collapsed to one-liner (wall kindness)" : "the full grammar wall was shortened + tailored to the query's keywords"}`);
|
|
12994
13221
|
}
|
|
12995
|
-
//
|
|
12996
|
-
//
|
|
12997
|
-
//
|
|
12998
|
-
//
|
|
12999
|
-
//
|
|
13000
|
-
//
|
|
13001
|
-
//
|
|
13002
|
-
|
|
13003
|
-
if (recordMiss && (via === "composed" || via === "miss")
|
|
13004
|
-
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
13005
|
-
if (adventureLive) {
|
|
13006
|
-
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>". Or ask the world: "look", "where is the key", "talk to the butler".)`;
|
|
13007
|
-
note(trace, "intermediate: HONEST-EMPTY POLISH — a live adventure miss points at the teach lane and the world asides, not the --repo remedy");
|
|
13008
|
-
} else if (browser) {
|
|
13009
|
-
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
|
|
13010
|
-
note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
|
|
13011
|
-
} else {
|
|
13012
|
-
answer = `${answer}\n(this repo has no code graph — index it with \`tmct index\`, point me at a \`.tmct/graph.json\` with \`--repo <path>\`, or run \`npm run example:mini\`.)`;
|
|
13013
|
-
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a tmct index/--repo pointer appended");
|
|
13014
|
-
}
|
|
13015
|
-
}
|
|
13016
|
-
// TEACH-OFFER: a "what is X" miss where X is genuinely unknown EVERYWHERE —
|
|
13017
|
-
// not a real graph entity, not a schema/vocab term, and not already in
|
|
13018
|
-
// memory — gets a short offer appended UNDER the existing miss text, never
|
|
13019
|
-
// replacing it.
|
|
13222
|
+
// TEACH-OFFER (computed first, applied after the polish below): a "what is
|
|
13223
|
+
// X" miss where X is genuinely unknown EVERYWHERE — not a real graph
|
|
13224
|
+
// entity, not a schema/vocab term, and not already in memory. Computed
|
|
13225
|
+
// ahead of the empty-graph polish because the two are alternative
|
|
13226
|
+
// recoveries for the same dead-end: a miss that is about to offer the
|
|
13227
|
+
// teach lane must not ALSO grow an index-this-repo pointer, or one
|
|
13228
|
+
// unparsed turn stacks three separate messages.
|
|
13229
|
+
let teachOffer = null;
|
|
13020
13230
|
if (recordMiss && (via === "composed" || via === "miss") && memoryDir) {
|
|
13021
13231
|
// "what do you know about X" is its OWN sibling shape — checked FIRST,
|
|
13022
13232
|
// without a resolveEntity(graph) gate: it's inherently a MEMORY question,
|
|
@@ -13027,7 +13237,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13027
13237
|
const offerSrc = expandContractions(String(query).trim());
|
|
13028
13238
|
const knowAboutTerm = offerSrc.match(KNOW_ABOUT_RE)?.[1]?.trim();
|
|
13029
13239
|
const offerTerm = knowAboutTerm || metaTermOf(offerSrc, envelope);
|
|
13030
|
-
|
|
13240
|
+
// A term that LEADS with a bindable anaphor ("it used for", from an
|
|
13241
|
+
// unresolved "what is it used for") is a pronoun that failed to bind,
|
|
13242
|
+
// not a teachable subject — offering to learn facts about it would echo
|
|
13243
|
+
// the garble back as an invitation to store it.
|
|
13244
|
+
const anaphorLedTerm = offerTerm && /^(?:it|this|that|these|those|them)\b/i.test(offerTerm.trim());
|
|
13245
|
+
if (offerTerm && !anaphorLedTerm) {
|
|
13031
13246
|
let normFactTerm;
|
|
13032
13247
|
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { normFactTerm = null; }
|
|
13033
13248
|
if (normFactTerm) {
|
|
@@ -13036,14 +13251,46 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13036
13251
|
if (!ent) {
|
|
13037
13252
|
const variants = factTermVariants(normFactTerm, offerTerm);
|
|
13038
13253
|
const known = (await memoryFacts(memoryDir)).some((f) => variants.has(f.subject) || variants.has(f.object));
|
|
13039
|
-
if (!known)
|
|
13040
|
-
answer = `${answer}\n${unknownVocabTermOffer(cleanTerm)}`;
|
|
13041
|
-
note(trace, `intermediate: TEACH-OFFER — "${cleanTerm}" is unknown to both the graph and memory, so the miss got an offer to learn appended`);
|
|
13042
|
-
}
|
|
13254
|
+
if (!known) teachOffer = unknownVocabTermOffer(cleanTerm);
|
|
13043
13255
|
}
|
|
13044
13256
|
}
|
|
13045
13257
|
}
|
|
13046
13258
|
}
|
|
13259
|
+
// #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
|
|
13260
|
+
// dead-end (an honest empty, the short miss, the bootstrap note) carries the
|
|
13261
|
+
// exit toward a real graph, unless it already points there — or unless the
|
|
13262
|
+
// turn already names its own recovery (a self-contained decline, or a
|
|
13263
|
+
// teach-offer about to land). Only when genuinely empty. The CLI keeps the
|
|
13264
|
+
// --repo/example pointer verbatim; a browser or a live adventure has no
|
|
13265
|
+
// such command to reach for, so each gets a teach-forward pointer (and the
|
|
13266
|
+
// adventure also names the world asides that are guaranteed to hit).
|
|
13267
|
+
// A live adventure keeps its polish even beside a teach-offer: the world
|
|
13268
|
+
// asides ("look", "talk to the butler") are guidance the offer can't carry.
|
|
13269
|
+
const adventureLive = !!planHolder?.state?.adventure;
|
|
13270
|
+
if (recordMiss && (via === "composed" || via === "miss") && !selfContainedMiss
|
|
13271
|
+
&& (adventureLive || !teachOffer)
|
|
13272
|
+
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
13273
|
+
if (adventureLive) {
|
|
13274
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>". Or ask the world: "look", "where is the key", "talk to the butler".)`;
|
|
13275
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a live adventure miss points at the teach lane and the world asides, not the --repo remedy");
|
|
13276
|
+
} else if (browser) {
|
|
13277
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
|
|
13278
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
|
|
13279
|
+
} else {
|
|
13280
|
+
answer = `${answer}\n(this repo has no code graph — index it with \`tmct index\`, point me at a \`.tmct/graph.json\` with \`--repo <path>\`, or run \`npm run example:mini\`.)`;
|
|
13281
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a tmct index/--repo pointer appended");
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
if (teachOffer) {
|
|
13285
|
+
// On a GENERIC wall (the shortened "couldn't read that", or the
|
|
13286
|
+
// graph-less bootstrap wall) the offer IS the whole answer — the wall
|
|
13287
|
+
// names no term, so keeping it above the offer stacks two messages
|
|
13288
|
+
// where one carries everything. A receipt-bearing specific miss keeps
|
|
13289
|
+
// the offer appended beneath it, unchanged.
|
|
13290
|
+
const genericWall = genericWallMiss || answer.startsWith(NO_GRAPH_BOOTSTRAP_WALL_LEAD);
|
|
13291
|
+
answer = genericWall ? teachOffer : `${answer}\n${teachOffer}`;
|
|
13292
|
+
note(trace, `intermediate: TEACH-OFFER — the term is unknown to both the graph and memory, so the miss ${genericWall ? "collapsed to the offer to learn" : "got an offer to learn appended"}`);
|
|
13293
|
+
}
|
|
13047
13294
|
// COLLISION RESTORE (pairs with relaxedTeachCollision, above): if nothing in
|
|
13048
13295
|
// the would-miss cascade actually stored/answered anything, fall back to
|
|
13049
13296
|
// the ORIGINAL ask-engine answer computed before this turn was forced into
|
|
@@ -13751,7 +13998,11 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
13751
13998
|
// the same pending state. Any other (real) query produces a fresh `last` without
|
|
13752
13999
|
// `pending`, so the remainder is naturally cleared — no stale continuation. ----
|
|
13753
14000
|
const PAGE = 32;
|
|
13754
|
-
|
|
14001
|
+
// "show the facts"/"show the chains" are the digest read-back's own escape
|
|
14002
|
+
// (the digest holds the full fact list — chains included, since the flat lines
|
|
14003
|
+
// carry their is-a ancestry — on the same pending remainder), folded in here so
|
|
14004
|
+
// they page the held list exactly as "more" does.
|
|
14005
|
+
const MORE_RE = /^(?:more|show more|see more|the rest|next|continue|go on|show(?: me)? the facts|show the chains)\b[.!?]*$/i;
|
|
13755
14006
|
|
|
13756
14007
|
/** The impact-intent gate — "what would break if I change X" and its natural
|
|
13757
14008
|
* neighbours, routed to the same /impact closure. Sibling of normalize.mjs's
|
|
@@ -14337,7 +14588,7 @@ function vocabAntecedentFrom(last) {
|
|
|
14337
14588
|
return m[1];
|
|
14338
14589
|
}
|
|
14339
14590
|
|
|
14340
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, _noSplit = false } = {}) {
|
|
14591
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, discourse = null, _noSplit = false } = {}) {
|
|
14341
14592
|
// Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
|
|
14342
14593
|
// bounds, the shared plan lane's search-depth cap) — a caller's own
|
|
14343
14594
|
// gameConfig (chat-session.mjs resolves one per session from tmct.toml)
|
|
@@ -14402,7 +14653,12 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14402
14653
|
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
14403
14654
|
// it untouched, and the caller re-threads whatever comes back.
|
|
14404
14655
|
const planHolder = { state: planState };
|
|
14405
|
-
|
|
14656
|
+
// The session's typed discourse record rides the same holder pattern,
|
|
14657
|
+
// threaded turn-to-turn beside focus and last. Registration happens where
|
|
14658
|
+
// an answer's typed content is in hand (runAsk, off the ask envelope's
|
|
14659
|
+
// `discourse` referents); the caller re-threads whatever comes back.
|
|
14660
|
+
const discourseHolder = { record: discourse ?? emptyDiscourseRecord() };
|
|
14661
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, discourseHolder, gameConfig: resolvedGameConfig, uiContext, synthesisBudget };
|
|
14406
14662
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
14407
14663
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
14408
14664
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -14446,10 +14702,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14446
14702
|
detail: finished.detail ?? null,
|
|
14447
14703
|
grounded: finished.record?.miss ? (last?.grounded ?? null) : finished.answer,
|
|
14448
14704
|
};
|
|
14705
|
+
// Every dispatched turn advances the discourse record's turn counter —
|
|
14706
|
+
// the counter is the registration ordinal that makes a same-turn tie
|
|
14707
|
+
// detectable, so it moves once, here, on the one path every dispatched
|
|
14708
|
+
// turn shares. Conversational turns bypass withLast and leave the record
|
|
14709
|
+
// untouched, exactly as they leave `last`.
|
|
14710
|
+
discourseHolder.record = advanceDiscourseTurn(discourseHolder.record);
|
|
14449
14711
|
// Goal/canonical lines append onto the PRE-narration `finished` result
|
|
14450
14712
|
// `nextLast` was captured from, so a narrated turn still gets both short
|
|
14451
14713
|
// lines up top plus the full trace block after.
|
|
14452
|
-
return { ...withNarration(withCanonicalLine(withGoalLine(finished)), trace, fallbackGoal), last: nextLast };
|
|
14714
|
+
return { ...withNarration(withCanonicalLine(withGoalLine(finished)), trace, fallbackGoal), last: nextLast, discourse: discourseHolder.record };
|
|
14453
14715
|
};
|
|
14454
14716
|
|
|
14455
14717
|
// Slash-optional system commands: a bare leading command word ("stats",
|
|
@@ -14692,17 +14954,18 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14692
14954
|
&& await everySentenceTeaches(sentences.slice(0, -1), lexicon);
|
|
14693
14955
|
const finalIsPayload = endsInPlanTrigger || teachesThenAsks;
|
|
14694
14956
|
if (finalIsPayload || await everySentenceTeaches(sentences, lexicon)) {
|
|
14695
|
-
let f = focus; let l = last; let ps = planHolder.state;
|
|
14957
|
+
let f = focus; let l = last; let ps = planHolder.state; let d = discourseHolder.record;
|
|
14696
14958
|
const receipts = [];
|
|
14697
14959
|
let finalRec = null;
|
|
14698
14960
|
for (const sentence of sentences) {
|
|
14699
14961
|
const r = await runTurn(sentence, {
|
|
14700
14962
|
config, source, graph, focus: f, last: l, memoryDir, sessionId, env, lexicon,
|
|
14701
|
-
narrate: false, vocabHint, tel, biasByBundle, planState: ps, _noSplit: true,
|
|
14963
|
+
narrate: false, vocabHint, tel, biasByBundle, planState: ps, discourse: d, _noSplit: true,
|
|
14702
14964
|
});
|
|
14703
14965
|
f = r.focus ?? f;
|
|
14704
14966
|
l = r.last ?? l;
|
|
14705
14967
|
if ("planState" in r) ps = r.planState;
|
|
14968
|
+
if ("discourse" in r) d = r.discourse;
|
|
14706
14969
|
finalRec = r;
|
|
14707
14970
|
receipts.push(String(r.answer ?? "").split("\n")[0]);
|
|
14708
14971
|
}
|
|
@@ -14725,6 +14988,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14725
14988
|
combined.planState = ps;
|
|
14726
14989
|
combined.focus = f;
|
|
14727
14990
|
combined.last = l;
|
|
14991
|
+
combined.discourse = d;
|
|
14728
14992
|
// Each per-sentence turn recorded only its OWN sentence; the transcript
|
|
14729
14993
|
// echo and the turn record must quote the whole multi-sentence line the
|
|
14730
14994
|
// user actually typed, not just its last sentence.
|