@polycode-projects/the-mechanical-code-talker 3.0.0 → 3.0.2
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 +98 -0
- 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 +13 -0
- package/src/domain/ask.mjs +53 -4
- package/src/domain/cli-verbs.mjs +11 -0
- package/src/domain/codeplan/graph-delta.mjs +239 -0
- package/src/domain/codeplan/graph-predicates.mjs +0 -0
- package/src/domain/codeplan/operators.mjs +232 -0
- package/src/domain/codeplan/planner.mjs +87 -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/services/chat-session.mjs +13 -1
- package/src/services/chat.mjs +191 -12
- package/src/services/ledger-viz.mjs +14 -3
- package/src/services/sessions.mjs +3 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +121 -116
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);
|
|
@@ -6411,6 +6436,37 @@ function senseSplitFactList(hits, rows, subjectVariants, { indent = "" } = {}) {
|
|
|
6411
6436
|
return { lines, grouped };
|
|
6412
6437
|
}
|
|
6413
6438
|
|
|
6439
|
+
// A subject-scan term answer longer than this many flat fact lines leads with a
|
|
6440
|
+
// deterministic digest paragraph (src/domain/digest) and holds the full list
|
|
6441
|
+
// behind the escape; a shorter answer is already readable as a list.
|
|
6442
|
+
const DIGEST_READBACK_THRESHOLD = 8;
|
|
6443
|
+
|
|
6444
|
+
/** The digest lead for a subject-scan term answer: a bounded narrative first
|
|
6445
|
+
* (selection, sentence structures, composition — all deterministic, no model),
|
|
6446
|
+
* the full fact list held behind the "show the facts"/"more" escape. `termRows`
|
|
6447
|
+
* are the term's own fact rows, `allRows` the whole store the statistics scan
|
|
6448
|
+
* over, `lines` the already-rendered flat fact list the escape reveals.
|
|
6449
|
+
*
|
|
6450
|
+
* Returns { text, pending } or null. Null when the structure bank is
|
|
6451
|
+
* unavailable (the in-browser dock stubs the filesystem loader out) or the
|
|
6452
|
+
* selector kept nothing renderable, so the caller falls back to the flat list —
|
|
6453
|
+
* the same graceful degradation the construction banks take in a browser
|
|
6454
|
+
* bundle. Deterministic; the digest reads only stored facts. */
|
|
6455
|
+
async function termDigestReadBack(term, termRows, allRows, lines) {
|
|
6456
|
+
let digestTermFromRows;
|
|
6457
|
+
try { ({ digestTermFromRows } = await import("../adapters/corpus/digest-bank.mjs")); }
|
|
6458
|
+
catch { return null; }
|
|
6459
|
+
let article;
|
|
6460
|
+
try { article = digestTermFromRows(term, termRows, allRows); }
|
|
6461
|
+
catch { return null; }
|
|
6462
|
+
if (!article || !article.paragraphs.length) return null;
|
|
6463
|
+
const sources = [...new Set((article.sources || []).map((s) => s.provenance).filter(Boolean))];
|
|
6464
|
+
const sourceLine = sources.length ? `(sources: ${sources.join("; ")})\n` : "";
|
|
6465
|
+
const escape = `Say 'show the facts' for all ${lines.length} stored facts.`;
|
|
6466
|
+
const text = `${article.paragraphs.join("\n\n")}\n\n${sourceLine}${escape}`;
|
|
6467
|
+
return { text, pending: { items: lines, noun: "facts" } };
|
|
6468
|
+
}
|
|
6469
|
+
|
|
6414
6470
|
/** "a"/"an" for a term, through the SAME grammar-rules.toml "article" rule and
|
|
6415
6471
|
* finish.mjs's beginsWithVowelSound every other agreement site in this file
|
|
6416
6472
|
* uses — never a hardcoded "a", which is ungrammatical for a vowel-initial
|
|
@@ -7538,7 +7594,17 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
7538
7594
|
// "disclosed, never dropped" contract). Unconfigured/tied bias degrades to
|
|
7539
7595
|
// trust-desc, byte-identical to before this feature existed.
|
|
7540
7596
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
7541
|
-
const
|
|
7597
|
+
const allRows = await factRows(memoryDir, cache);
|
|
7598
|
+
const { lines, grouped } = senseSplitFactList(hits, allRows, variants);
|
|
7599
|
+
// A long undifferentiated "what is X" leads with the digest — a bounded
|
|
7600
|
+
// narrative over the same facts — and holds the full list behind the escape.
|
|
7601
|
+
// It wins over the sense-split grouping here: the digest's own selector
|
|
7602
|
+
// filters the mis-sensed branch that grouping would otherwise surface as its
|
|
7603
|
+
// own block. Falls back to grouping/flat when the digest is unavailable.
|
|
7604
|
+
const digested = (!predicate && lines.length > DIGEST_READBACK_THRESHOLD)
|
|
7605
|
+
? await termDigestReadBack(subject, hits, allRows, lines)
|
|
7606
|
+
: null;
|
|
7607
|
+
if (digested) return { ...digested, replace: miss };
|
|
7542
7608
|
if (grouped) return { ...grouped, replace: miss };
|
|
7543
7609
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
7544
7610
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
@@ -11813,7 +11879,31 @@ const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s
|
|
|
11813
11879
|
* than silently accepted alongside the current location. */
|
|
11814
11880
|
const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
|
|
11815
11881
|
|
|
11816
|
-
|
|
11882
|
+
/** "was that before logger.mjs was touched" — a singular bindable form, a
|
|
11883
|
+
* comparison word, and an embedded passive clause. The closed participle set
|
|
11884
|
+
* is the touch family the when-question path answers; anything else keeps
|
|
11885
|
+
* the honest miss. */
|
|
11886
|
+
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;
|
|
11887
|
+
|
|
11888
|
+
/** ARCHITECTURE-OVERVIEW intent — "show me the architecture", "what is the
|
|
11889
|
+
* architecture of this repo": the whole-repo map the /arch command renders.
|
|
11890
|
+
* A closed phrase set, because the literal word "architecture" is also a
|
|
11891
|
+
* plausible SYMBOL substring in many graphs (renderArchitecture,
|
|
11892
|
+
* tmct_architecture) — the symbol-describe rescues would otherwise resolve
|
|
11893
|
+
* the word to one such symbol and dump its definition card instead of the
|
|
11894
|
+
* map. Every phrasing here names the architecture as a TOPIC (an article,
|
|
11895
|
+
* an of-this-repo tail, or an overview/map noun); a query that NAMES a
|
|
11896
|
+
* symbol ("describe renderArchitecture") never matches. */
|
|
11897
|
+
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+)?";
|
|
11898
|
+
const ARCH_OVERVIEW_TAIL = "(?:\\s+(?:of|for)\\s+(?:this|the)\\s+(?:app|codebase|repo|repository|project|code))?";
|
|
11899
|
+
const ARCH_OVERVIEW_PHRASES = [
|
|
11900
|
+
// Article-carried: "the architecture" alone, or wrapped/tailed.
|
|
11901
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}the\\s+architecture(?:\\s+(?:overview|map|diagram))?${ARCH_OVERVIEW_TAIL}(?:\\s+here)?\\??$`, "i"),
|
|
11902
|
+
// Article-less: anchored by the of-this-repo tail or the overview/map noun instead.
|
|
11903
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}architecture\\s+(?:(?:of|for)\\s+(?:this|the)\\s+(?:app|codebase|repo|repository|project|code)|overview|map|diagram)\\??$`, "i"),
|
|
11904
|
+
];
|
|
11905
|
+
|
|
11906
|
+
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
11907
|
const ts = new Date().toISOString();
|
|
11818
11908
|
// The surface this turn runs on ("cli" default; "browser" from a web entry) —
|
|
11819
11909
|
// the honest-miss tail below points a browser/adventure miss at the teach
|
|
@@ -11842,6 +11932,45 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11842
11932
|
.replace(/^(?:and|so|then|also)\s+/i, "")
|
|
11843
11933
|
.replace(/how many\s+/i, "how many of those ");
|
|
11844
11934
|
}
|
|
11935
|
+
// TEMPORAL COMPARISON ACROSS TURNS — "was that before logger.mjs was
|
|
11936
|
+
// touched": a singular bindable form, a comparison word, and an embedded
|
|
11937
|
+
// passive clause. The form binds against the session's discourse record (a
|
|
11938
|
+
// dated referent a previous answer established), the embedded clause runs
|
|
11939
|
+
// fresh through the same when-question path a standalone turn takes, and
|
|
11940
|
+
// the two ISO dates compare with both sides cited. Checked BEFORE the ask
|
|
11941
|
+
// engine (the same precedence RENAME_HISTORY_RE takes, below) so the
|
|
11942
|
+
// sentence never reaches the keyword-spot strategy's multi-token patient
|
|
11943
|
+
// guard; an unbound form, an undated referent, or an unanswerable clause
|
|
11944
|
+
// all fall through, and that guard's honest miss stands unchanged.
|
|
11945
|
+
{
|
|
11946
|
+
const cmp = graph && discourseHolder ? String(query).trim().match(TEMPORAL_COMPARISON_RE) : null;
|
|
11947
|
+
if (cmp) {
|
|
11948
|
+
const [, form, cmpOp, clauseSubject, participle] = cmp;
|
|
11949
|
+
const bound = bindDiscourseForm(discourseHolder.record, form);
|
|
11950
|
+
const refDay = String(bound?.referent?.attrs?.date || "").slice(0, 10);
|
|
11951
|
+
if (bound?.referent && refDay) {
|
|
11952
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
11953
|
+
const fresh = ask(graph, `when was ${clauseSubject} ${participle}`);
|
|
11954
|
+
const freshHit = (!fresh?.tmct_ask?.miss && !fresh?.tmct_ask?.ambiguous) ? fresh?.tmct_ask?.matches?.[0] : null;
|
|
11955
|
+
const freshCommit = freshHit?.id ? graph.byId?.get?.(freshHit.id) : null;
|
|
11956
|
+
const clauseDay = freshCommit?.class === "Commit"
|
|
11957
|
+
? String((freshCommit.attributes || []).find((a) => a.key === "date")?.value || "").slice(0, 10)
|
|
11958
|
+
: "";
|
|
11959
|
+
if (clauseDay) {
|
|
11960
|
+
const holds = cmpOp.toLowerCase() === "before" ? refDay < clauseDay : refDay > clauseDay;
|
|
11961
|
+
const relation = refDay < clauseDay ? "came before" : refDay > clauseDay ? "came after" : "landed on the same day as";
|
|
11962
|
+
const verb = participle.toLowerCase();
|
|
11963
|
+
const text = `${holds ? "Yes" : "No"} — ${bound.referent.label} (${refDay}) ${relation} ${clauseSubject} was last ${verb} (${freshCommit.label}, ${clauseDay}).`;
|
|
11964
|
+
note(trace, "goal: compare a prior answer's dated referent against a freshly read event (cross-turn temporal composition)");
|
|
11965
|
+
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`);
|
|
11966
|
+
const turn = plainTurn(query, text, { via: "composed", miss: false, focus });
|
|
11967
|
+
const cited = [graph.byId?.get?.(bound.referent.ids[0]), freshCommit].filter(Boolean);
|
|
11968
|
+
turn.detail = { traversal: `discourse ${bound.referent.ref} (${refDay}) vs last-${verb} of ${clauseSubject} (${clauseDay})`, matches: cited };
|
|
11969
|
+
return turn;
|
|
11970
|
+
}
|
|
11971
|
+
}
|
|
11972
|
+
}
|
|
11973
|
+
}
|
|
11845
11974
|
// RENAME HISTORY — "what was X called before" and its siblings. The index
|
|
11846
11975
|
// records current names only, and without this gate "called" fuzzes onto
|
|
11847
11976
|
// the calls relation ("before" simply drops), so the reply read as fluent
|
|
@@ -12020,6 +12149,17 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12020
12149
|
"isn't resolved to anything yet — name the term directly, or ask a question that resolves one first.",
|
|
12021
12150
|
);
|
|
12022
12151
|
if (envJson) { try { envelope = JSON.parse(envJson); } catch { envelope = null; } }
|
|
12152
|
+
// Typed discourse referents the answer established (the ask envelope's
|
|
12153
|
+
// additive `discourse` field, emitted beside the eval where the answer's
|
|
12154
|
+
// content is still typed) register into the session's record here — the
|
|
12155
|
+
// one point both ask paths (direct call and dispatchTool) converge.
|
|
12156
|
+
if (discourseHolder && Array.isArray(envelope?.discourse)) {
|
|
12157
|
+
for (const { lane, ...spec } of envelope.discourse) {
|
|
12158
|
+
discourseHolder.record = registerReferent(discourseHolder.record, {
|
|
12159
|
+
...spec, from: { turn: discourseHolder.record.turn, lane, query: askQuery },
|
|
12160
|
+
});
|
|
12161
|
+
}
|
|
12162
|
+
}
|
|
12023
12163
|
} catch (e) {
|
|
12024
12164
|
const thrown = String(e?.message || e);
|
|
12025
12165
|
// A graph-less session's ask dispatch fails reading the never-configured
|
|
@@ -12198,10 +12338,32 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12198
12338
|
note(trace, `goal: ${deduced} (revised — the raw \"what else\" phrasing was recognized directly, not the relaxed/reparsed envelope)`);
|
|
12199
12339
|
}
|
|
12200
12340
|
}
|
|
12341
|
+
// (0a) ARCHITECTURE OVERVIEW — see ARCH_OVERVIEW_PHRASES. Answered here,
|
|
12342
|
+
// before the meta/orientation lanes and long before the symbol-resolve
|
|
12343
|
+
// rescues (4d), so the closed architecture phrasings reach the map instead
|
|
12344
|
+
// of a literal-token symbol card or the vocabulary-touch teach offer.
|
|
12345
|
+
if (!handled && miss && graph && !noCodeGraph(graph)) {
|
|
12346
|
+
// The RAW text is tried alongside the peeled one: applyPreambleFrames'
|
|
12347
|
+
// show/give-me bridge rewrites "show me the architecture" into "describe
|
|
12348
|
+
// architecture", which drops the article this closed set anchors on.
|
|
12349
|
+
const archRaw = correctMisspellings(String(query).trim());
|
|
12350
|
+
const archPeeled = applyPreambleFrames(archRaw);
|
|
12351
|
+
if (ARCH_OVERVIEW_PHRASES.some((re) => re.test(archRaw) || re.test(archPeeled))) {
|
|
12352
|
+
try {
|
|
12353
|
+
const archText = await dispatchTool("tmct_architecture", {}, { config, source, tel });
|
|
12354
|
+
if (archText) {
|
|
12355
|
+
answer = archText; via = "meta"; recordMiss = false; handled = true;
|
|
12356
|
+
deduced = "understand the overall architecture (package/module boundaries)";
|
|
12357
|
+
note(trace, `goal: ${deduced} (revised — a closed architecture-overview phrasing was recognized directly)`);
|
|
12358
|
+
note(trace, "lane: (0a) ARCHITECTURE OVERVIEW — routed to the whole-repo architecture map (/arch), never a literal symbol lookup on the word \"architecture\"");
|
|
12359
|
+
}
|
|
12360
|
+
} catch { /* the tool couldn't load a graph — the ordinary lanes decide */ }
|
|
12361
|
+
}
|
|
12362
|
+
}
|
|
12201
12363
|
// (1) #2 META/SELF: bare self/session questions ("what do you know", "what is this
|
|
12202
12364
|
// codebase", "how do i start") → a summary / orientation, answered before the
|
|
12203
12365
|
// fact-dump readers so "what do you know" gets a summary, not raw facts.
|
|
12204
|
-
if (miss) {
|
|
12366
|
+
if (!handled && miss) {
|
|
12205
12367
|
const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint, focus });
|
|
12206
12368
|
if (meta) {
|
|
12207
12369
|
// A lane may answer with a better-worded decline (the module-orient
|
|
@@ -13751,7 +13913,11 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
13751
13913
|
// the same pending state. Any other (real) query produces a fresh `last` without
|
|
13752
13914
|
// `pending`, so the remainder is naturally cleared — no stale continuation. ----
|
|
13753
13915
|
const PAGE = 32;
|
|
13754
|
-
|
|
13916
|
+
// "show the facts"/"show the chains" are the digest read-back's own escape
|
|
13917
|
+
// (the digest holds the full fact list — chains included, since the flat lines
|
|
13918
|
+
// carry their is-a ancestry — on the same pending remainder), folded in here so
|
|
13919
|
+
// they page the held list exactly as "more" does.
|
|
13920
|
+
const MORE_RE = /^(?:more|show more|see more|the rest|next|continue|go on|show(?: me)? the facts|show the chains)\b[.!?]*$/i;
|
|
13755
13921
|
|
|
13756
13922
|
/** The impact-intent gate — "what would break if I change X" and its natural
|
|
13757
13923
|
* neighbours, routed to the same /impact closure. Sibling of normalize.mjs's
|
|
@@ -14337,7 +14503,7 @@ function vocabAntecedentFrom(last) {
|
|
|
14337
14503
|
return m[1];
|
|
14338
14504
|
}
|
|
14339
14505
|
|
|
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 } = {}) {
|
|
14506
|
+
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
14507
|
// Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
|
|
14342
14508
|
// bounds, the shared plan lane's search-depth cap) — a caller's own
|
|
14343
14509
|
// gameConfig (chat-session.mjs resolves one per session from tmct.toml)
|
|
@@ -14402,7 +14568,12 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14402
14568
|
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
14403
14569
|
// it untouched, and the caller re-threads whatever comes back.
|
|
14404
14570
|
const planHolder = { state: planState };
|
|
14405
|
-
|
|
14571
|
+
// The session's typed discourse record rides the same holder pattern,
|
|
14572
|
+
// threaded turn-to-turn beside focus and last. Registration happens where
|
|
14573
|
+
// an answer's typed content is in hand (runAsk, off the ask envelope's
|
|
14574
|
+
// `discourse` referents); the caller re-threads whatever comes back.
|
|
14575
|
+
const discourseHolder = { record: discourse ?? emptyDiscourseRecord() };
|
|
14576
|
+
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
14577
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
14407
14578
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
14408
14579
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -14446,10 +14617,16 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14446
14617
|
detail: finished.detail ?? null,
|
|
14447
14618
|
grounded: finished.record?.miss ? (last?.grounded ?? null) : finished.answer,
|
|
14448
14619
|
};
|
|
14620
|
+
// Every dispatched turn advances the discourse record's turn counter —
|
|
14621
|
+
// the counter is the registration ordinal that makes a same-turn tie
|
|
14622
|
+
// detectable, so it moves once, here, on the one path every dispatched
|
|
14623
|
+
// turn shares. Conversational turns bypass withLast and leave the record
|
|
14624
|
+
// untouched, exactly as they leave `last`.
|
|
14625
|
+
discourseHolder.record = advanceDiscourseTurn(discourseHolder.record);
|
|
14449
14626
|
// Goal/canonical lines append onto the PRE-narration `finished` result
|
|
14450
14627
|
// `nextLast` was captured from, so a narrated turn still gets both short
|
|
14451
14628
|
// lines up top plus the full trace block after.
|
|
14452
|
-
return { ...withNarration(withCanonicalLine(withGoalLine(finished)), trace, fallbackGoal), last: nextLast };
|
|
14629
|
+
return { ...withNarration(withCanonicalLine(withGoalLine(finished)), trace, fallbackGoal), last: nextLast, discourse: discourseHolder.record };
|
|
14453
14630
|
};
|
|
14454
14631
|
|
|
14455
14632
|
// Slash-optional system commands: a bare leading command word ("stats",
|
|
@@ -14692,17 +14869,18 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14692
14869
|
&& await everySentenceTeaches(sentences.slice(0, -1), lexicon);
|
|
14693
14870
|
const finalIsPayload = endsInPlanTrigger || teachesThenAsks;
|
|
14694
14871
|
if (finalIsPayload || await everySentenceTeaches(sentences, lexicon)) {
|
|
14695
|
-
let f = focus; let l = last; let ps = planHolder.state;
|
|
14872
|
+
let f = focus; let l = last; let ps = planHolder.state; let d = discourseHolder.record;
|
|
14696
14873
|
const receipts = [];
|
|
14697
14874
|
let finalRec = null;
|
|
14698
14875
|
for (const sentence of sentences) {
|
|
14699
14876
|
const r = await runTurn(sentence, {
|
|
14700
14877
|
config, source, graph, focus: f, last: l, memoryDir, sessionId, env, lexicon,
|
|
14701
|
-
narrate: false, vocabHint, tel, biasByBundle, planState: ps, _noSplit: true,
|
|
14878
|
+
narrate: false, vocabHint, tel, biasByBundle, planState: ps, discourse: d, _noSplit: true,
|
|
14702
14879
|
});
|
|
14703
14880
|
f = r.focus ?? f;
|
|
14704
14881
|
l = r.last ?? l;
|
|
14705
14882
|
if ("planState" in r) ps = r.planState;
|
|
14883
|
+
if ("discourse" in r) d = r.discourse;
|
|
14706
14884
|
finalRec = r;
|
|
14707
14885
|
receipts.push(String(r.answer ?? "").split("\n")[0]);
|
|
14708
14886
|
}
|
|
@@ -14725,6 +14903,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14725
14903
|
combined.planState = ps;
|
|
14726
14904
|
combined.focus = f;
|
|
14727
14905
|
combined.last = l;
|
|
14906
|
+
combined.discourse = d;
|
|
14728
14907
|
// Each per-sentence turn recorded only its OWN sentence; the transcript
|
|
14729
14908
|
// echo and the turn record must quote the whole multi-sentence line the
|
|
14730
14909
|
// user actually typed, not just its last sentence.
|
|
@@ -504,8 +504,8 @@ function sparkCaptionHtml(stats) {
|
|
|
504
504
|
* different questions (did this render even offer the reference; did the
|
|
505
505
|
* browser actually manage to load it), and both must hold for the live
|
|
506
506
|
* path to run. */
|
|
507
|
-
export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle, stats, ledgerBundleAvailable = false } = {}) {
|
|
508
|
-
const ledgerJson = embedJson({ rows: rows || [], terms: terms || [], edges: edges || [], focus: focus || null, contradictions: contradictions || [], worthALook: worthALook || null, meta: meta || { shown: 0, total: 0, truncated: false } });
|
|
507
|
+
export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, worthALook, payload, meta, memoryAskBundle, stats, ledgerBundleAvailable = false, focusDigest = null } = {}) {
|
|
508
|
+
const ledgerJson = embedJson({ rows: rows || [], terms: terms || [], edges: edges || [], focus: focus || null, contradictions: contradictions || [], worthALook: worthALook || null, meta: meta || { shown: 0, total: 0, truncated: false }, focusDigest: focusDigest || null });
|
|
509
509
|
const payloadJson = embedJson(payload || { individuals: [], objectProperties: [] });
|
|
510
510
|
const shown = meta?.shown ?? (rows || []).length;
|
|
511
511
|
const title = `tmct ledger — ${shown} fact${shown === 1 ? "" : "s"}${focus ? ` (focus: ${escapeHtml(focus)})` : ""}`;
|
|
@@ -641,6 +641,9 @@ ${THEME_TOKENS_CSS}
|
|
|
641
641
|
.focuscard { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: .8rem 1rem; margin-bottom: 1rem; }
|
|
642
642
|
.focuscard .term { font-size: 1.35rem; font-weight: 700; }
|
|
643
643
|
.focuscard .klass, .focuscard .stats { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); margin-top: .3rem; font-variant-numeric: tabular-nums; }
|
|
644
|
+
.focuscard .focusdigest { margin-top: .7rem; font-size: .92rem; line-height: 1.5; }
|
|
645
|
+
.focuscard .focusdigest p { margin: 0 0 .4rem; }
|
|
646
|
+
.focuscard .focusdigest .dgsrc { font-family: ${MONO_STACK}; font-size: .7rem; color: var(--muted); margin-top: .2rem; }
|
|
644
647
|
.group { margin: 1.1rem 0; }
|
|
645
648
|
.group h3 { font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 400; margin: 0 0 .35rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
646
649
|
.group h3::after { content: ""; flex: 1; border-top: 1px solid var(--line); transform: translateY(-.2em); }
|
|
@@ -860,10 +863,18 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
860
863
|
const srcs = new Set(all.map((r) => r.src.split(" | ")[0]));
|
|
861
864
|
const dates = all.map((r) => r.createdAt).filter(Boolean).sort();
|
|
862
865
|
const klass = all.find((r) => r.s === focus && r.family === "is-a");
|
|
866
|
+
// The digest paragraph is computed server-side for the page's initial focus
|
|
867
|
+
// (renderLedgerHtml has the payload and reads the sentence-structure bank);
|
|
868
|
+
// a client refocus to another term shows the fact groups without it.
|
|
869
|
+
const dg = (focus === LEDGER.focus && LEDGER.focusDigest && LEDGER.focusDigest.paragraphs && LEDGER.focusDigest.paragraphs.length) ? LEDGER.focusDigest : null;
|
|
870
|
+
const digestHtml = dg
|
|
871
|
+
? '<div class="focusdigest">' + dg.paragraphs.map((p) => "<p>" + esc(p) + "</p>").join("") +
|
|
872
|
+
(dg.sources && dg.sources.length ? '<p class="dgsrc">sources: ' + esc(dg.sources.join("; ")) + "</p>" : "") + "</div>"
|
|
873
|
+
: "";
|
|
863
874
|
let html = '<div class="focuscard"><div class="term">' + esc(focus) + "</div>" +
|
|
864
875
|
'<div class="klass">' + (klass ? esc(klass.phrase + " " + klass.o) : "no class recorded") + "</div>" +
|
|
865
876
|
'<div class="stats">' + all.length + " facts · " + srcs.size + " source" + (srcs.size === 1 ? "" : "s") +
|
|
866
|
-
(dates.length ? " · first " + esc(dates[0].slice(0, 10)) + " · last " + esc(dates[dates.length - 1].slice(0, 10)) : "") + "</div
|
|
877
|
+
(dates.length ? " · first " + esc(dates[0].slice(0, 10)) + " · last " + esc(dates[dates.length - 1].slice(0, 10)) : "") + "</div>" + digestHtml + "</div>";
|
|
867
878
|
const bracketed = new Set();
|
|
868
879
|
for (const fam of FAMS) {
|
|
869
880
|
const rows = mine.filter((r) => r.family === fam && !bracketed.has(r.id));
|
|
@@ -48,7 +48,9 @@ function labelFromId(id) {
|
|
|
48
48
|
* reader never sees a torn graph.json and a crash never destroys the old one. */
|
|
49
49
|
async function atomicWriteJson(file, obj) {
|
|
50
50
|
const tmp = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
51
|
-
|
|
51
|
+
// 2-space indent: a legitimate session upsert into a hand-formatted graph
|
|
52
|
+
// reflows to the same pretty shape a producer wrote, not one compact line.
|
|
53
|
+
await writeFile(tmp, JSON.stringify(obj, null, 2));
|
|
52
54
|
await rename(tmp, file);
|
|
53
55
|
}
|
|
54
56
|
|