@polycode-projects/the-mechanical-code-talker 3.0.6 → 3.0.8
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 +35 -10
- package/bin/tmct.mjs +1 -1
- package/package.json +18 -15
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/ask-vocab.mjs +4 -0
- package/src/domain/ask.mjs +101 -12
- package/src/domain/codegraph.mjs +25 -8
- package/src/domain/interpret/normalize.mjs +2 -2
- package/src/domain/reference-pack.mjs +12 -3
- package/src/domain/router/call-validator.mjs +1 -1
- package/src/domain/router/drive.mjs +2 -2
- package/src/domain/router/set-algebra.mjs +1 -1
- package/src/services/adventure.mjs +34 -19
- package/src/services/chat-page-viz.mjs +1 -1
- package/src/services/chat.mjs +172 -39
- package/src/services/index.mjs +6 -3
- package/src/services/ledger-viz.mjs +1 -1
- package/src/surfaces/web/chat-browser-entry.mjs +1 -1
- package/src/surfaces/web/digest-client.mjs +8 -3
- package/src/surfaces/web/graph-ask-browser-entry.mjs +41 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +86 -86
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
12
12
|
import { parseImperative, OBJECT_PRONOUNS } from "../domain/grammar/ace.mjs";
|
|
13
|
+
import { register as registerReferent, bind as bindDiscourseForm } from "../domain/discourse.mjs";
|
|
13
14
|
import { createCompletionsGraphAdapter } from "../domain/completions/graph-adapter.mjs";
|
|
14
15
|
import { actionFamilies } from "../domain/router/taught.mjs";
|
|
15
16
|
import { compileDomain, precondHolds, roleBinding } from "../domain/domain.mjs";
|
|
@@ -1224,14 +1225,16 @@ function renderedImperativeCommand(cmd) {
|
|
|
1224
1225
|
return parts.join(" ");
|
|
1225
1226
|
}
|
|
1226
1227
|
|
|
1227
|
-
// ---- pronoun binding: the session
|
|
1228
|
+
// ---- pronoun binding: the session discourse record ----------------------------
|
|
1228
1229
|
//
|
|
1229
1230
|
// A world command may name its object with a pronoun ("examine it", "take
|
|
1230
1231
|
// them", "talk to him") instead of a noun. The antecedent is not in the
|
|
1231
|
-
// sentence — it's the last
|
|
1232
|
-
// session
|
|
1232
|
+
// sentence — it's the last adventure object the player successfully acted on
|
|
1233
|
+
// this session — so the parser leaves the pronoun bare (ace.mjs's
|
|
1233
1234
|
// OBJECT_PRONOUNS) and the lane binds it here, through ONE seam that every
|
|
1234
|
-
// object-taking verb passes on its way to runWorldCommand.
|
|
1235
|
+
// object-taking verb passes on its way to runWorldCommand. The antecedent lives
|
|
1236
|
+
// as one referent among N in the shared discourse record, so an adventure
|
|
1237
|
+
// object is bound the same way a code-graph answer's referent is. With nothing
|
|
1235
1238
|
// standing, a pronoun gets an honest reference nudge, never the vocabulary
|
|
1236
1239
|
// decline (a pronoun is a reference, not an unknown word).
|
|
1237
1240
|
|
|
@@ -1264,19 +1267,25 @@ async function noFocusPronounNudge(pronoun, { memoryDir }) {
|
|
|
1264
1267
|
);
|
|
1265
1268
|
}
|
|
1266
1269
|
|
|
1267
|
-
/** Bind any pronoun object/indirect/instrument slot to the
|
|
1268
|
-
* Returns `{ cmd }` with the pronouns
|
|
1269
|
-
* nudge }` (the reference nudge) when a
|
|
1270
|
-
*
|
|
1271
|
-
|
|
1270
|
+
/** Bind any pronoun object/indirect/instrument slot to the last adventure
|
|
1271
|
+
* object in the discourse record. Returns `{ cmd }` with the pronouns
|
|
1272
|
+
* rewritten to that object's term, or `{ nudge }` (the reference nudge) when a
|
|
1273
|
+
* pronoun stands but no adventure object does. A command with no pronoun
|
|
1274
|
+
* passes straight through untouched. All four surface pronouns
|
|
1275
|
+
* (it/them/him/her) normalize to the one `it` probe, then bind to the newest
|
|
1276
|
+
* referent THIS lane registered — the record may also hold code-graph
|
|
1277
|
+
* referents, so the bind is scoped to `lane: "adventure"`. */
|
|
1278
|
+
async function bindPronouns(cmd, { discourseHolder, memoryDir }) {
|
|
1272
1279
|
if (!commandHasPronoun(cmd)) return { cmd };
|
|
1273
|
-
|
|
1280
|
+
const probe = discourseHolder ? bindDiscourseForm(discourseHolder.record, "it") : null;
|
|
1281
|
+
const focusTerm = (probe?.candidates || []).find((r) => r.from?.lane === "adventure")?.label ?? null;
|
|
1282
|
+
if (!focusTerm) {
|
|
1274
1283
|
const pronoun = PRONOUN_SLOTS.map((s) => cmd[s]).find((v) => v && OBJECT_PRONOUNS.has(v));
|
|
1275
1284
|
return { nudge: await noFocusPronounNudge(pronoun, { memoryDir }) };
|
|
1276
1285
|
}
|
|
1277
1286
|
const bound = { ...cmd };
|
|
1278
1287
|
for (const s of PRONOUN_SLOTS) {
|
|
1279
|
-
if (bound[s] && OBJECT_PRONOUNS.has(bound[s])) bound[s] =
|
|
1288
|
+
if (bound[s] && OBJECT_PRONOUNS.has(bound[s])) bound[s] = focusTerm;
|
|
1280
1289
|
}
|
|
1281
1290
|
return { cmd: bound };
|
|
1282
1291
|
}
|
|
@@ -1292,7 +1301,7 @@ async function bindPronouns(cmd, { focus, memoryDir }) {
|
|
|
1292
1301
|
* recognizer, injected so the two lanes can never disagree about what a plan
|
|
1293
1302
|
* frame is.
|
|
1294
1303
|
*/
|
|
1295
|
-
export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "", env, lexicon = null, graph = null, cache = null, isPlanFrameLine = () => false }) {
|
|
1304
|
+
export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "", env, lexicon = null, graph = null, cache = null, isPlanFrameLine = () => false, discourseHolder = null }) {
|
|
1296
1305
|
const slot = planHolder?.state ?? null;
|
|
1297
1306
|
const adventure = slot?.adventure ?? null;
|
|
1298
1307
|
const opening = matchAdventureOpening(line);
|
|
@@ -1351,16 +1360,22 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
|
|
|
1351
1360
|
if (INVENTORY_RE.test(line)) return inventoryAnswer({ memoryDir, graph });
|
|
1352
1361
|
const parsed = parseImperative(line, lexicon ?? undefined);
|
|
1353
1362
|
if (parsed) {
|
|
1354
|
-
const bound = await bindPronouns(parsed, {
|
|
1363
|
+
const bound = await bindPronouns(parsed, { discourseHolder, memoryDir });
|
|
1355
1364
|
if (bound.nudge) return bound.nudge;
|
|
1356
1365
|
const cmd = bound.cmd;
|
|
1357
1366
|
const result = await runWorldCommand(cmd, { world: adventure.world, memoryDir, env, graph, cache });
|
|
1358
|
-
// The object a command SUCCESSFULLY named
|
|
1359
|
-
// pronoun binds to — so "look lamp" then "examine it" reads the
|
|
1360
|
-
// "talk to housekeeper" makes "him"/"her" the housekeeper. A miss
|
|
1361
|
-
// the
|
|
1362
|
-
//
|
|
1363
|
-
if (!result.miss && cmd.object
|
|
1367
|
+
// The object a command SUCCESSFULLY named registers as a discourse referent
|
|
1368
|
+
// a later pronoun binds to — so "look lamp" then "examine it" reads the
|
|
1369
|
+
// lamp, and "talk to housekeeper" makes "him"/"her" the housekeeper. A miss
|
|
1370
|
+
// leaves the record untouched; a bare room look or a move carries no object
|
|
1371
|
+
// and so never disturbs it.
|
|
1372
|
+
if (!result.miss && cmd.object && discourseHolder) {
|
|
1373
|
+
discourseHolder.record = registerReferent(discourseHolder.record, {
|
|
1374
|
+
kind: "entity", class: "AdventureObject", label: cmd.object,
|
|
1375
|
+
ids: [`adventure:${cmd.object}`], attrs: {},
|
|
1376
|
+
from: { turn: discourseHolder.record.turn, lane: "adventure", query: line },
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1364
1379
|
if (!cmd.corrected?.length) return result;
|
|
1365
1380
|
// A fuzzy-repaired verb or direction still executes normally, but the
|
|
1366
1381
|
// response says what it read the line as, so a genuine miss is never
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// chip is read straight off the SAME "(source: ...)" citation chat.mjs's own
|
|
17
17
|
// factPhrase/renderFactLine convention already appends to most answers (see
|
|
18
18
|
// e.g. `dog is a kind of animal (source: corpus:conceptnet ...)` — already
|
|
19
|
-
// asserted by e2e/pages-chat-fullscreen.test.mjs against this page), never a
|
|
19
|
+
// asserted by test-e2e/pages-chat-fullscreen.test.mjs against this page), never a
|
|
20
20
|
// second provenance computation against memory internals: `provBucketFor`
|
|
21
21
|
// (ledger-viz.mjs) is spliced in unmodified and applied to whatever citation
|
|
22
22
|
// text the answer already carries, so this page's chip and the ledger's own
|
package/src/services/chat.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { join, dirname } from "node:path";
|
|
21
21
|
import { dispatchTool, loadGraph, TOOLS } from "../tools/server.mjs";
|
|
22
22
|
import { ToolError } from "../adapters/config.mjs";
|
|
23
|
-
import { parseEntities, edgesOfKind, moduleCountOf, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "../domain/codegraph.mjs";
|
|
23
|
+
import { parseEntities, edgesOfKind, moduleCountOf, packageCounts, modulesOf, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "../domain/codegraph.mjs";
|
|
24
24
|
import { classDisplayName, DYNAMIC_TAIL_OK_RE } from "../domain/ask.mjs";
|
|
25
25
|
import { emptyRecord as emptyDiscourseRecord, advanceTurn as advanceDiscourseTurn, register as registerReferent, bind as bindDiscourseForm } from "../domain/discourse.mjs";
|
|
26
26
|
import { uuidv7 } from "../adapters/uuid.mjs";
|
|
@@ -462,6 +462,7 @@ const COUNT_NOUNS = {
|
|
|
462
462
|
class: "Class", classes: "Class",
|
|
463
463
|
function: "Function", functions: "Function", func: "Function", funcs: "Function",
|
|
464
464
|
module: "Module", modules: "Module", file: "Module", files: "Module",
|
|
465
|
+
package: "Package", packages: "Package",
|
|
465
466
|
method: "Method", methods: "Method",
|
|
466
467
|
attribute: "Attribute", attributes: "Attribute",
|
|
467
468
|
variable: "GlobalVariable", variables: "GlobalVariable", global: "GlobalVariable", globals: "GlobalVariable",
|
|
@@ -472,18 +473,25 @@ const COUNT_NOUNS = {
|
|
|
472
473
|
/** class → [singular, plural] display noun, for echoing a count back in English. */
|
|
473
474
|
const CLASS_LABELS = {
|
|
474
475
|
Class: ["class", "classes"], Function: ["function", "functions"],
|
|
475
|
-
Module: ["module", "modules"],
|
|
476
|
+
Module: ["module", "modules"], Package: ["package", "packages"],
|
|
477
|
+
Method: ["method", "methods"],
|
|
476
478
|
Attribute: ["attribute", "attributes"], GlobalVariable: ["variable", "variables"],
|
|
477
479
|
Commit: ["commit", "commits"], Session: ["session", "sessions"],
|
|
478
480
|
};
|
|
479
481
|
const classNoun = (cls, n) => { const [s, p] = CLASS_LABELS[cls] || [cls, `${cls}s`]; return n === 1 ? s : p; };
|
|
480
482
|
|
|
481
|
-
/** Count individuals of a class in the loaded graph (live, not the header field).
|
|
482
|
-
|
|
483
|
+
/** Count individuals of a class in the loaded graph (live, not the header field).
|
|
484
|
+
* No individual is ever stored with class "Package" — packages are the
|
|
485
|
+
* directories grouping the modules, derived the same way the architecture map
|
|
486
|
+
* derives them. */
|
|
487
|
+
const countClass = (graph, cls) => (cls === "Package"
|
|
488
|
+
? packageCounts(modulesOf(graph)).size
|
|
489
|
+
: graph.individuals.filter((i) => (i.class || "") === cls).length);
|
|
483
490
|
|
|
484
491
|
/** The classes this graph can actually count, as a human list ("classes, functions, …"). */
|
|
485
492
|
function countableKinds(graph) {
|
|
486
493
|
const present = new Set(graph.individuals.map((i) => i.class).filter(Boolean));
|
|
494
|
+
if (present.has("Module")) present.add("Package");
|
|
487
495
|
return Object.keys(CLASS_LABELS).filter((c) => present.has(c)).map((c) => CLASS_LABELS[c][1]);
|
|
488
496
|
}
|
|
489
497
|
|
|
@@ -1079,13 +1087,15 @@ const CAPABILITY_PHRASES = [
|
|
|
1079
1087
|
/^(?: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,
|
|
1080
1088
|
/^(?:give me|what's) the lay of the land\??$/i,
|
|
1081
1089
|
// "give me an overview" / "an overview" — the plain-word sibling of "the
|
|
1082
|
-
// big picture" just above
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1090
|
+
// big picture" just above. Without a closed entry the word "overview"
|
|
1091
|
+
// prose-matches real symbols in an indexed graph (moduleOverviewText) and
|
|
1092
|
+
// the describe rescue dumps that symbol's card. The detailed forms ("give
|
|
1093
|
+
// me a detailed overview of X") carry a mandatory "detailed"+of-term and
|
|
1094
|
+
// stay with the completions rescue, untouched by this anchor. An explicit
|
|
1095
|
+
// of-this-repo tail is NOT matched here: that names the repo as the
|
|
1096
|
+
// subject, so ARCH_OVERVIEW_PHRASES answers it with the architecture map
|
|
1097
|
+
// instead of this card.
|
|
1098
|
+
/^(?:(?:can|could|would) you\s+)?(?:give me|show me|gimme)\s+an overview(?:\s+here)?\??$/i,
|
|
1089
1099
|
/^an overview(?:\s+please)?\??$/i,
|
|
1090
1100
|
// "what have we got here"/"what've we got here" — a casual, self-answering
|
|
1091
1101
|
// opener (matches after a leading "so" strips via LEADING_CONNECTIVE_RE,
|
|
@@ -1563,15 +1573,8 @@ const BYE = new Set([
|
|
|
1563
1573
|
"bye", "goodbye", "quit", "exit", "see ya", "see you", "cya", "later", "farewell",
|
|
1564
1574
|
"peace", "peace out", "im off", "i'm off", "gtg", "gotta go", "catch you later",
|
|
1565
1575
|
"farewell then",
|
|
1566
|
-
// "gtg thx" — the SAME "gtg" farewell above, immediately followed by a
|
|
1567
|
-
// thanks word with no delimiter between them (so farewellOrThanksSignal's
|
|
1568
|
-
// comma/semicolon clause split never sees two clauses to work with).
|
|
1569
|
-
// Whole-phrase entry rather than a general "bye word + thanks word, no
|
|
1570
|
-
// delimiter" mechanism — closed and hand-curated, this exact reported
|
|
1571
|
-
// phrasing only.
|
|
1572
|
-
"gtg thx",
|
|
1573
1576
|
// "good day to you" deliberately does NOT live here: it's a formal-register
|
|
1574
|
-
// GREETING, not a farewell.
|
|
1577
|
+
// GREETING, not a farewell. farewellClause is checked before GREET in
|
|
1575
1578
|
// conversationalTurn, so having it here would silently end the session on
|
|
1576
1579
|
// a plain formal greeting — every turn piped after it dropped with no log
|
|
1577
1580
|
// entry, a worse outcome than any wall.
|
|
@@ -1677,15 +1680,39 @@ const CLAUSE_SPLIT_RE = /\s*[,;]\s*(?:and\s+)?|\s+and\s+/;
|
|
|
1677
1680
|
function conversationalClauses(q) {
|
|
1678
1681
|
return q.split(CLAUSE_SPLIT_RE).map((c) => c.trim()).filter(Boolean);
|
|
1679
1682
|
}
|
|
1680
|
-
/**
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
|
|
1683
|
+
/** A thanks phrase and a bye phrase butted together with NO delimiter between
|
|
1684
|
+
* them, in either order ("gtg thx", "thanks bye"). conversationalClauses
|
|
1685
|
+
* splits on a comma, semicolon or a standalone "and", so these lines arrive
|
|
1686
|
+
* as one clause and match neither closed set whole. Splitting at each word
|
|
1687
|
+
* boundary and matching the two halves against their own sets keeps both
|
|
1688
|
+
* halves closed — no new literal, and no phrase is recognized here that
|
|
1689
|
+
* isn't already recognized alone. Without this the leading thanks word reads
|
|
1690
|
+
* as a bare subject noun and the farewell reads as its verb, so "thanks bye"
|
|
1691
|
+
* parses as a habitual-teach ("a thank can bye"). */
|
|
1692
|
+
function gluedThanksBye(clause) {
|
|
1693
|
+
const words = clause.split(/\s+/).filter(Boolean);
|
|
1694
|
+
for (let split = 1; split < words.length; split += 1) {
|
|
1695
|
+
const head = words.slice(0, split).join(" ");
|
|
1696
|
+
const tail = words.slice(split).join(" ");
|
|
1697
|
+
const headIsBye = !!closedOrCollapsed(head, BYE, BYE_COLLAPSED);
|
|
1698
|
+
const tailIsBye = !!closedOrCollapsed(tail, BYE, BYE_COLLAPSED);
|
|
1699
|
+
const headIsThanks = !!closedOrCollapsed(head, THANKS, THANKS_COLLAPSED);
|
|
1700
|
+
const tailIsThanks = !!closedOrCollapsed(tail, THANKS, THANKS_COLLAPSED);
|
|
1701
|
+
if ((headIsBye && tailIsThanks) || (headIsThanks && tailIsBye)) return true;
|
|
1702
|
+
}
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
/** Does this clause end the session? The closed BYE set, or informal
|
|
1706
|
+
* reduplication of one of its entries ("bye bye", "no no" — general, not
|
|
1707
|
+
* specific to any one word), or a thanks word glued to a bye word. Shared by
|
|
1708
|
+
* the single-clause whole-line check and the multi-clause scan below, so
|
|
1709
|
+
* "bye bye" resolves the same way whether or not a comma follows it. Bye wins
|
|
1710
|
+
* over thanks: a farewell should end the session even alongside gratitude. */
|
|
1711
|
+
function farewellClause(clause) {
|
|
1686
1712
|
if (closedOrCollapsed(clause, BYE, BYE_COLLAPSED)) return true;
|
|
1687
1713
|
const folded = clause.match(REPEATED_WORD_RE);
|
|
1688
|
-
|
|
1714
|
+
if (folded && closedOrCollapsed(folded[1], BYE, BYE_COLLAPSED)) return true;
|
|
1715
|
+
return gluedThanksBye(clause);
|
|
1689
1716
|
}
|
|
1690
1717
|
/** Closing-filler clauses — the CONTENT half of a farewell/thanks sentence
|
|
1691
1718
|
* ("thanks, that's everything for now") that isn't itself gratitude or bye
|
|
@@ -1739,7 +1766,7 @@ function farewellOrThanksSignal(raw, q) {
|
|
|
1739
1766
|
const rawClause = clauses[i];
|
|
1740
1767
|
const ackMatch = rawClause.match(ACK_LEAD_RE);
|
|
1741
1768
|
const clause = ackMatch ? ackMatch[1].trim() : rawClause;
|
|
1742
|
-
if (
|
|
1769
|
+
if (farewellClause(clause)) { byeHit = true; break; }
|
|
1743
1770
|
const deIntensified = clause.replace(THANKS_HELP_TAIL_RE, "").replace(TRAILING_INTENSIFIER_RE, "").trim();
|
|
1744
1771
|
if (thanksClauseIdx < 0 && closedOrCollapsed(deIntensified, THANKS, THANKS_COLLAPSED)) thanksClauseIdx = i;
|
|
1745
1772
|
}
|
|
@@ -1877,9 +1904,9 @@ function conversationalTurn(line, ctx) {
|
|
|
1877
1904
|
...(end ? { end: true } : {}),
|
|
1878
1905
|
}, ctx.trace);
|
|
1879
1906
|
};
|
|
1880
|
-
if (
|
|
1907
|
+
if (farewellClause(q)) {
|
|
1881
1908
|
note(ctx.trace, "goal: casual/social — ending the session (no graph intent)");
|
|
1882
|
-
note(ctx.trace, "lane: conversational — farewell (BYE closed set, incl. bare reduplication e.g. \"bye bye\")");
|
|
1909
|
+
note(ctx.trace, "lane: conversational — farewell (BYE closed set, incl. bare reduplication e.g. \"bye bye\" and a thanks word glued to a bye word e.g. \"thanks bye\")");
|
|
1883
1910
|
return mk(t(T_FAREWELL), { end: true });
|
|
1884
1911
|
}
|
|
1885
1912
|
{
|
|
@@ -6696,7 +6723,7 @@ function suggestibleSubjectPhrase(subject) {
|
|
|
6696
6723
|
* take the article. */
|
|
6697
6724
|
const QUANTIFIER_LEAD_RE = /^(?:every|each|all|any)\s+/i;
|
|
6698
6725
|
|
|
6699
|
-
function factTermVariants(normFactTerm, term) {
|
|
6726
|
+
export function factTermVariants(normFactTerm, term) {
|
|
6700
6727
|
const t = normFactTerm(term);
|
|
6701
6728
|
const v = new Set();
|
|
6702
6729
|
// The ask frames glue a quantifier onto the subject and looked up "every
|
|
@@ -8440,7 +8467,7 @@ const CARD_EXISTENCE_ASK_RE = /^does\s+an?\s+(.+?)\s+have\s+an?\s+(.+?)[?.!\s]*$
|
|
|
8440
8467
|
/** The 4 pattern-5 cardinality-restriction predicates buildCardinalityRestrictions
|
|
8441
8468
|
* reconstructs from — owl:onProperty (shared scaffolding with someValuesFrom
|
|
8442
8469
|
* restrictions too) is added alongside this set by each reader below, not
|
|
8443
|
-
* folded into it here, mirroring infbench/grade.mjs's own identically-named
|
|
8470
|
+
* folded into it here, mirroring test-benchmarks/infbench/grade.mjs's own identically-named
|
|
8444
8471
|
* set + separate owl:onProperty handling. */
|
|
8445
8472
|
const CARDINALITY_ROW_PREDICATES = new Set(["owl:cardinality", "owl:minCardinality", "owl:maxCardinality", "owl:onClass"]);
|
|
8446
8473
|
/** "who owns <X>" / "who maintains <X>" — the closed ownership read-back over
|
|
@@ -11215,7 +11242,7 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
11215
11242
|
const definition = (await seonDefinitions()).get(term) ?? null;
|
|
11216
11243
|
if (!definition) return null;
|
|
11217
11244
|
// The runChat shell hands the loaded graph straight in; the pure runTurn(config)
|
|
11218
|
-
// path (tests, chatbench) does not, so load it the same way dispatchTool does when
|
|
11245
|
+
// path (tests, test-benchmarks/chatbench) does not, so load it the same way dispatchTool does when
|
|
11219
11246
|
// it's missing. Failure-tolerated: no loadable graph → no concept force.
|
|
11220
11247
|
let g = graph;
|
|
11221
11248
|
if (!g && config && source) {
|
|
@@ -11984,6 +12011,15 @@ const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)
|
|
|
11984
12011
|
* the honest miss. */
|
|
11985
12012
|
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;
|
|
11986
12013
|
|
|
12014
|
+
/** "were those before logger.mjs was touched" — the plural sibling of
|
|
12015
|
+
* TEMPORAL_COMPARISON_RE. A plural bindable form (`those`/`them`/`these`)
|
|
12016
|
+
* binds a `set` referent a listing/filter answer established, and the
|
|
12017
|
+
* comparison runs over the whole set: each member is dated from the graph,
|
|
12018
|
+
* and the answer quantifies (all / none / M of N) rather than forcing one
|
|
12019
|
+
* date. A set whose members are not all datable refuses, the same honest
|
|
12020
|
+
* miss the singular lane takes on an undated referent. */
|
|
12021
|
+
const PLURAL_TEMPORAL_COMPARISON_RE = /^(?:were|are)\s+(those|them|these)\s+(before|after)\s+(.+?)\s+(?:was|were)\s+(touched|changed|modified|edited|updated)[?.!\s]*$/i;
|
|
12022
|
+
|
|
11987
12023
|
/** ARCHITECTURE-OVERVIEW intent — "show me the architecture", "what is the
|
|
11988
12024
|
* architecture of this repo": the whole-repo map the /arch command renders.
|
|
11989
12025
|
* A closed phrase set, because the literal word "architecture" is also a
|
|
@@ -11994,12 +12030,17 @@ const TEMPORAL_COMPARISON_RE = /^(?:was|is)\s+(this one|that one|it|this|that)\s
|
|
|
11994
12030
|
* an of-this-repo tail, or an overview/map noun); a query that NAMES a
|
|
11995
12031
|
* symbol ("describe renderArchitecture") never matches. */
|
|
11996
12032
|
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+)?";
|
|
11997
|
-
const
|
|
12033
|
+
const ARCH_OVERVIEW_OF_REPO = "(?:of|for)\\s+(?:this|the)\\s+(?:app|codebase|repo|repository|project|code)";
|
|
12034
|
+
const ARCH_OVERVIEW_TAIL = `(?:\\s+${ARCH_OVERVIEW_OF_REPO})?`;
|
|
11998
12035
|
const ARCH_OVERVIEW_PHRASES = [
|
|
11999
12036
|
// Article-carried: "the architecture" alone, or wrapped/tailed.
|
|
12000
12037
|
new RegExp(`^${ARCH_OVERVIEW_LEAD}the\\s+architecture(?:\\s+(?:overview|map|diagram))?${ARCH_OVERVIEW_TAIL}(?:\\s+here)?\\??$`, "i"),
|
|
12001
12038
|
// Article-less: anchored by the of-this-repo tail or the overview/map noun instead.
|
|
12002
|
-
new RegExp(`^${ARCH_OVERVIEW_LEAD}architecture\\s+(
|
|
12039
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}architecture\\s+(?:${ARCH_OVERVIEW_OF_REPO}|overview|map|diagram)\\??$`, "i"),
|
|
12040
|
+
// Architecture-less: the topic noun stands in for the word, so the of-this-repo
|
|
12041
|
+
// tail is REQUIRED here. A bare "give me an overview" names no subject and
|
|
12042
|
+
// falls through to the ordinary lanes.
|
|
12043
|
+
new RegExp(`^${ARCH_OVERVIEW_LEAD}(?:(?:an?|the)\\s+)?(?:overview|map|diagram)\\s+${ARCH_OVERVIEW_OF_REPO}(?:\\s+here)?\\??$`, "i"),
|
|
12003
12044
|
];
|
|
12004
12045
|
|
|
12005
12046
|
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 }) {
|
|
@@ -12012,9 +12053,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12012
12053
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
12013
12054
|
// ask()'s `prev`. Prefers the FULL id set (`allIds`) over `matches`, since a
|
|
12014
12055
|
// concept-force listing caps `matches` at MAX_EXAMPLES.
|
|
12015
|
-
|
|
12056
|
+
let prev = (last?.detail?.allIds && last.detail.allIds.length)
|
|
12016
12057
|
? last.detail.allIds.filter(Boolean)
|
|
12017
12058
|
: (last?.detail?.matches || []).map((m) => m?.id).filter(Boolean);
|
|
12059
|
+
// A count erases its own matches to [], so the previous turn carries no id
|
|
12060
|
+
// set for the next "which of those" to narrow. When the carry is empty, fall
|
|
12061
|
+
// back to the session record: a standing `set` referent that "those" binds
|
|
12062
|
+
// keeps a narrowed set alive across a counting hop. No same-turn plural tie
|
|
12063
|
+
// is reachable yet (no lane registers two set referents in one turn), so this
|
|
12064
|
+
// probe needs no tie refusal.
|
|
12065
|
+
if (!prev.length && discourseHolder) {
|
|
12066
|
+
const boundSet = bindDiscourseForm(discourseHolder.record, "those");
|
|
12067
|
+
if (boundSet?.referent?.ids?.length) prev = boundSet.referent.ids.filter(Boolean);
|
|
12068
|
+
}
|
|
12018
12069
|
// The query the ENGINE parses: a "what about X" continuation is rewritten to the
|
|
12019
12070
|
// prior shape with X swapped in; everything else parses verbatim. The record and
|
|
12020
12071
|
// transcript keep the user's ACTUAL words (`query`), only the parse target changes.
|
|
@@ -12055,7 +12106,11 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12055
12106
|
note(trace, `lane: TEMPORAL_COMPARISON_RE — "${form}" could not compose a comparison; a specific miss names why, never the teach-offer cascade`);
|
|
12056
12107
|
return plainTurn(query, text, { via: "miss", miss: true, focus, goal: temporalGoal });
|
|
12057
12108
|
};
|
|
12058
|
-
const bound = discourseHolder ? bindDiscourseForm(discourseHolder.record, form) : null;
|
|
12109
|
+
const bound = discourseHolder ? bindDiscourseForm(discourseHolder.record, form, { tieRefuses: true }) : null;
|
|
12110
|
+
if (bound?.tie) {
|
|
12111
|
+
const options = joinOr(bound.tie.map((r) => r.label));
|
|
12112
|
+
return refMiss(`"${form}" could mean ${options} — which do you mean?`);
|
|
12113
|
+
}
|
|
12059
12114
|
if (!bound?.referent) {
|
|
12060
12115
|
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.`);
|
|
12061
12116
|
}
|
|
@@ -12087,6 +12142,80 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12087
12142
|
return turn;
|
|
12088
12143
|
}
|
|
12089
12144
|
}
|
|
12145
|
+
// TEMPORAL COMPARISON OVER A PLURAL ANTECEDENT — "were those before
|
|
12146
|
+
// logger.mjs was touched": the plural sibling of the block above. `those`
|
|
12147
|
+
// binds a `set` referent a prior listing/filter established, and the whole
|
|
12148
|
+
// set is compared against the freshly read clause date. Each member is dated
|
|
12149
|
+
// from the graph (the set referent carries member ids, not per-member dates,
|
|
12150
|
+
// so the dates are read here the same way the clause is read fresh), and the
|
|
12151
|
+
// answer quantifies over the set — all, none, or M of N — rather than
|
|
12152
|
+
// forcing the set onto a single date. A set whose members are not all
|
|
12153
|
+
// datable refuses honestly, and an unbound form, an undatable clause, or a
|
|
12154
|
+
// missing graph keep today's miss, the same specific declines the singular
|
|
12155
|
+
// lane makes. Checked here, before the ask engine, for the same reason the
|
|
12156
|
+
// singular lane is: otherwise "those before logger.mjs was" reaches the
|
|
12157
|
+
// keyword-spot strategy as multi-token patient wreckage and the teach-offer
|
|
12158
|
+
// cascade reads it as a subject to learn facts about.
|
|
12159
|
+
{
|
|
12160
|
+
const cmp = String(query).trim().match(PLURAL_TEMPORAL_COMPARISON_RE);
|
|
12161
|
+
if (cmp) {
|
|
12162
|
+
const [, form, cmpOp, clauseSubject, participle] = cmp;
|
|
12163
|
+
const verb = participle.toLowerCase();
|
|
12164
|
+
const temporalGoal = "compare a prior answer's dated set against a freshly read event (cross-turn temporal composition over a plural antecedent)";
|
|
12165
|
+
const refMiss = (text) => {
|
|
12166
|
+
note(trace, `goal: ${temporalGoal}`);
|
|
12167
|
+
note(trace, `lane: PLURAL_TEMPORAL_COMPARISON_RE — "${form}" could not compose a set comparison; a specific miss names why, never the teach-offer cascade`);
|
|
12168
|
+
return plainTurn(query, text, { via: "miss", miss: true, focus, goal: temporalGoal });
|
|
12169
|
+
};
|
|
12170
|
+
const bound = discourseHolder ? bindDiscourseForm(discourseHolder.record, form, { tieRefuses: true }) : null;
|
|
12171
|
+
if (bound?.tie) {
|
|
12172
|
+
const options = joinOr(bound.tie.map((r) => r.label));
|
|
12173
|
+
return refMiss(`"${form}" could mean ${options} — which set do you mean?`);
|
|
12174
|
+
}
|
|
12175
|
+
if (!bound?.referent) {
|
|
12176
|
+
return refMiss(`I don't have a set for "${form}" yet — nothing answered earlier in this conversation binds it. Ask for the set first (e.g. "what changed before <commit>"), then ask the comparison again.`);
|
|
12177
|
+
}
|
|
12178
|
+
const set = bound.referent;
|
|
12179
|
+
const total = set.ids.length;
|
|
12180
|
+
if (!graph) {
|
|
12181
|
+
return refMiss(`"${form}" refers to ${set.label}, but I need a code graph to date its members and when ${clauseSubject} was last ${verb} — no code graph is loaded.`);
|
|
12182
|
+
}
|
|
12183
|
+
const dateOfId = (id) => {
|
|
12184
|
+
const ind = graph.byId?.get?.(id);
|
|
12185
|
+
return ind ? String((ind.attributes || []).find((a) => a.key === "date")?.value || "").slice(0, 10) : "";
|
|
12186
|
+
};
|
|
12187
|
+
const memberDates = set.ids.map(dateOfId);
|
|
12188
|
+
const undated = memberDates.filter((d) => !d).length;
|
|
12189
|
+
if (undated) {
|
|
12190
|
+
return refMiss(`"${form}" refers to ${set.label}, but not every member carries a date I can compare — ${undated} of the ${total} have no date on record, so I can't place the set before or after ${clauseSubject} was ${verb}.`);
|
|
12191
|
+
}
|
|
12192
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
12193
|
+
const fresh = ask(graph, `when was ${clauseSubject} ${participle}`);
|
|
12194
|
+
const freshHit = (!fresh?.tmct_ask?.miss && !fresh?.tmct_ask?.ambiguous) ? fresh?.tmct_ask?.matches?.[0] : null;
|
|
12195
|
+
const freshCommit = freshHit?.id ? graph.byId?.get?.(freshHit.id) : null;
|
|
12196
|
+
const clauseDay = freshCommit?.class === "Commit"
|
|
12197
|
+
? String((freshCommit.attributes || []).find((a) => a.key === "date")?.value || "").slice(0, 10)
|
|
12198
|
+
: "";
|
|
12199
|
+
if (!clauseDay) {
|
|
12200
|
+
return refMiss(`"${form}" refers to ${set.label}, but I couldn't date when ${clauseSubject} was last ${verb} in this index — so I can't compare the set to it.`);
|
|
12201
|
+
}
|
|
12202
|
+
const before = cmpOp.toLowerCase() === "before";
|
|
12203
|
+
const satisfied = memberDates.filter((d) => before ? d < clauseDay : d > clauseDay).length;
|
|
12204
|
+
const relation = before ? "came before" : "came after";
|
|
12205
|
+
const tail = `${clauseSubject} was last ${verb} (${freshCommit.label}, ${clauseDay})`;
|
|
12206
|
+
const text = satisfied === total
|
|
12207
|
+
? `Yes — all ${set.label} ${relation} ${tail}.`
|
|
12208
|
+
: satisfied === 0
|
|
12209
|
+
? `No — none of the ${set.label} ${relation} ${tail}.`
|
|
12210
|
+
: `Partly — ${satisfied} of the ${set.label} ${relation} ${tail}; the other ${total - satisfied} did not.`;
|
|
12211
|
+
note(trace, `goal: ${temporalGoal}`);
|
|
12212
|
+
note(trace, `lane: PLURAL_TEMPORAL_COMPARISON_RE — "${form}" bound ${set.label} (${total} dated members) through the discourse record; ${satisfied}/${total} ${relation} the freshly read clause (${clauseDay})`);
|
|
12213
|
+
const turn = plainTurn(query, text, { via: "composed", miss: false, focus, goal: temporalGoal });
|
|
12214
|
+
const cited = [...set.ids.map((id) => graph.byId?.get?.(id)), freshCommit].filter(Boolean);
|
|
12215
|
+
turn.detail = { traversal: `discourse ${set.ref} (${total} members) vs last-${verb} of ${clauseSubject} (${clauseDay})`, matches: cited };
|
|
12216
|
+
return turn;
|
|
12217
|
+
}
|
|
12218
|
+
}
|
|
12090
12219
|
// RENAME HISTORY — "what was X called before" and its siblings. The index
|
|
12091
12220
|
// records current names only, and without this gate "called" fuzzes onto
|
|
12092
12221
|
// the calls relation ("before" simply drops), so the reply read as fluent
|
|
@@ -12272,10 +12401,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12272
12401
|
// content is still typed) register into the session's record here — the
|
|
12273
12402
|
// one point both ask paths (direct call and dispatchTool) converge.
|
|
12274
12403
|
if (discourseHolder && Array.isArray(envelope?.discourse)) {
|
|
12275
|
-
for (const { lane, ...spec } of envelope.discourse) {
|
|
12404
|
+
for (const { lane, bound, ...spec } of envelope.discourse) {
|
|
12276
12405
|
discourseHolder.record = registerReferent(discourseHolder.record, {
|
|
12277
12406
|
...spec, from: { turn: discourseHolder.record.turn, lane, query: askQuery },
|
|
12278
|
-
});
|
|
12407
|
+
}, { bound: !!bound });
|
|
12279
12408
|
}
|
|
12280
12409
|
}
|
|
12281
12410
|
} catch (e) {
|
|
@@ -14126,6 +14255,10 @@ function matchImpactIntent(line) {
|
|
|
14126
14255
|
return null;
|
|
14127
14256
|
}
|
|
14128
14257
|
const joinList = (a) => (a.length > 1 ? `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}` : (a[0] ?? ""));
|
|
14258
|
+
// A discourse tie lists its options with a short Oxford-comma "or" join,
|
|
14259
|
+
// deliberately not the resolver's longer numbered ambiguity format.
|
|
14260
|
+
const joinOr = (a) => (a.length > 2 ? `${a.slice(0, -1).join(", ")}, or ${a[a.length - 1]}`
|
|
14261
|
+
: a.length === 2 ? `${a[0]} or ${a[1]}` : (a[0] ?? ""));
|
|
14129
14262
|
|
|
14130
14263
|
/** Render the next page of a held remainder (pending: {items:[str], noun}). Returns a
|
|
14131
14264
|
* plain turn whose `detail.pending` carries what's still unseen (null when the batch
|
|
@@ -14813,7 +14946,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14813
14946
|
// otherwise read as a declarative or an orientation ask.
|
|
14814
14947
|
{
|
|
14815
14948
|
const advTurn = await adventureTurn(workingLine, {
|
|
14816
|
-
planHolder, memoryDir, sessionId, env, lexicon, graph, cache: factRowsCache, isPlanFrameLine,
|
|
14949
|
+
planHolder, memoryDir, sessionId, env, lexicon, graph, cache: factRowsCache, isPlanFrameLine, discourseHolder,
|
|
14817
14950
|
});
|
|
14818
14951
|
if (advTurn) {
|
|
14819
14952
|
note(trace, `lane: ${advTurn.note}`);
|
package/src/services/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// ask.mjs keeps the core primitives (resolveObject, traverse, render) and
|
|
7
7
|
// the ask() orchestration.
|
|
8
8
|
|
|
9
|
-
// Chat surface
|
|
9
|
+
// Chat surface.
|
|
10
10
|
// createSession is the library's session sink — the same focus/last/planState
|
|
11
11
|
// threading and memory-backend seam every shell shares; runTurn is the pure
|
|
12
12
|
// single-turn engine underneath it.
|
|
@@ -35,6 +35,9 @@ setConstructionBanks(readConstructionFiles);
|
|
|
35
35
|
// Graph traversal primitives.
|
|
36
36
|
export { relationKind, impactClosure } from "../domain/codegraph.mjs";
|
|
37
37
|
|
|
38
|
+
// Graph-service construction for a provider-supplied code graph.
|
|
39
|
+
export { createGraphService } from "../adapters/providers/graph-service.mjs";
|
|
40
|
+
|
|
38
41
|
// Tool dispatch (slash-commands and CLI tool calls route through here).
|
|
39
42
|
export { dispatchTool } from "../tools/server.mjs";
|
|
40
43
|
|
|
@@ -49,13 +52,13 @@ export { foldSessionLogs } from "./fold.mjs";
|
|
|
49
52
|
// fetchEntities() is the one read path.
|
|
50
53
|
export { fetchEntities, registerProvider } from "../adapters/source.mjs";
|
|
51
54
|
|
|
52
|
-
// `tmct init` onboarding
|
|
55
|
+
// `tmct init` onboarding.
|
|
53
56
|
// init.mjs and toml-config.mjs each export a same-named `CONFIG_FILE`
|
|
54
57
|
// constant ("tmct.toml") — aliased here so both can ride the one `.` entry
|
|
55
58
|
// point without colliding.
|
|
56
59
|
export { initRepo, defaultConfig, renderTomlConfig, PERSONA_PRESETS, CONFIG_FILE as INIT_CONFIG_FILE } from "./init.mjs";
|
|
57
60
|
|
|
58
|
-
// tmct.toml loading
|
|
61
|
+
// tmct.toml loading.
|
|
59
62
|
export { CONFIG_FILE as TOML_CONFIG_FILE } from "../adapters/toml-config.mjs";
|
|
60
63
|
|
|
61
64
|
// The "detailed answer" completions pipeline (also reachable as the
|
|
@@ -795,7 +795,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
795
795
|
// LEDGER (declared "let" in the embed script above this one — a classic,
|
|
796
796
|
// non-module script's top-level bindings are reachable as bare identifiers
|
|
797
797
|
// by every later script tag in the document, never as window.LEDGER; see
|
|
798
|
-
// e2e/pages-ledger.test.mjs's own note on this) and its indexes start as
|
|
798
|
+
// test-e2e/pages-ledger.test.mjs's own note on this) and its indexes start as
|
|
799
799
|
// the server-rendered snapshot but are REASSIGNED wholesale after a live
|
|
800
800
|
// teach (applyLedgerData, below) — a successful teach through the dock
|
|
801
801
|
// changes the underlying graph, and the page has to show that, not keep
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// Under the bundle build, scripts/build-chat-bundle.mjs swaps that import
|
|
21
21
|
// for a live in-memory twin, and this call actually feeds it the page's
|
|
22
22
|
// embedded structure rows; this file is ALSO imported directly by plain
|
|
23
|
-
// Node (e2e/web-chat-memory.test.mjs, exercising the same engine contract
|
|
23
|
+
// Node (test-e2e/web-chat-memory.test.mjs, exercising the same engine contract
|
|
24
24
|
// without a browser), where the import resolves to the real fs+TOML
|
|
25
25
|
// adapter — its own setDigestStructures is a documented no-op there
|
|
26
26
|
// (see that module's header), so the call is harmless either way, never
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { buildStructureTable, digestTerm } from "../../domain/digest/index.mjs";
|
|
16
16
|
import { digestStoreStats, chainsForObjects, isaObjectsOf } from "../../domain/digest/store-stats.mjs";
|
|
17
|
-
import { readFactRows } from "../../adapters/memory/core.mjs";
|
|
17
|
+
import { readFactRows, normFactTerm } from "../../adapters/memory/core.mjs";
|
|
18
|
+
import { factTermVariants } from "../../services/chat.mjs";
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Digest one term end to end from fact rows and a pre-parsed structure table.
|
|
@@ -40,14 +41,18 @@ export function digestTermFromRowsBrowser(term, termRows, allRows, structures, o
|
|
|
40
41
|
* in-browser surface already holds (the ledger's embedded PAYLOAD, the ledger
|
|
41
42
|
* dock's live memoryDir.payload, the research session's store). Scans the
|
|
42
43
|
* payload once for the term's own rows and the whole-store statistics, so a
|
|
43
|
-
* caller never has to run readFactRows itself.
|
|
44
|
+
* caller never has to run readFactRows itself. Matches `term` against a fact's
|
|
45
|
+
* subject through the same spelling-variant fold `factTermVariants` gives the
|
|
46
|
+
* "what is X" fact reader (plural/irregular-plural folding), so a term seeded
|
|
47
|
+
* as "dog" is still found when queried as "dogs".
|
|
44
48
|
*
|
|
45
49
|
* Returns the render-ready view (see digestViewFromArticle) or null when the
|
|
46
50
|
* term has no rows, no structures were supplied, or the selector kept nothing.
|
|
47
51
|
*/
|
|
48
52
|
export function digestTermFromPayloadBrowser(payload, term, structures, opts = {}) {
|
|
49
53
|
const rows = readFactRows(payload || { individuals: [], objectProperties: [] });
|
|
50
|
-
const
|
|
54
|
+
const variants = factTermVariants(normFactTerm, term);
|
|
55
|
+
const termRows = rows.filter((r) => variants.has(r.subject));
|
|
51
56
|
if (!termRows.length) return null;
|
|
52
57
|
const article = digestTermFromRowsBrowser(term, termRows, rows, structures, opts);
|
|
53
58
|
return digestViewFromArticle(article);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// graph-ask-browser-entry.mjs — the public, browser-safe entry point for
|
|
2
|
+
// tmct's CODE-graph query engine (`./ask-browser` package export).
|
|
3
|
+
//
|
|
4
|
+
// A plain re-export, zero side effects, zero `globalThis` assignment: unlike
|
|
5
|
+
// this directory's other *-browser-entry.mjs files (built into an IIFE and
|
|
6
|
+
// inlined into a specific tmct page), this one is meant to be imported by a
|
|
7
|
+
// DOWNSTREAM consumer's own bundler (e.g. seonix's website chat panel) as a
|
|
8
|
+
// genuine ESM module, not inlined by tmct's own build scripts.
|
|
9
|
+
//
|
|
10
|
+
// It exists because the package root (`src/services/index.mjs`, and its
|
|
11
|
+
// `.`/exports subpath) also runs Node-side composition wiring at module-load
|
|
12
|
+
// time — `setDefaultNlpAdapter(nlpAdapter)` from `../adapters/ask-nlp.mjs`
|
|
13
|
+
// (wink-nlp, a ~4MB model) and `setConstructionBanks(readConstructionFiles)`
|
|
14
|
+
// from `../adapters/corpus/construction-banks.mjs` (fs reads) — neither of
|
|
15
|
+
// which can run or bundle in a browser. This file imports only ask.mjs and
|
|
16
|
+
// graph-service.mjs directly, so none of that wiring is reachable from here.
|
|
17
|
+
//
|
|
18
|
+
// Verified with `esbuild --bundle --platform=browser --format=esm`
|
|
19
|
+
// (scripts/verify-ask-browser-entry.mjs). ask()'s optional strategies
|
|
20
|
+
// (ace.mjs, constructions.mjs) and the wink lemma/POS adapter are all reached
|
|
21
|
+
// through nlp-registry.mjs's `defaultNlp()` registry seam and each call
|
|
22
|
+
// site's own `typeof X !== "undefined"` guard, not a static import, so a
|
|
23
|
+
// consumer bundling this entry point alone never links wink-nlp or any
|
|
24
|
+
// fs-reading corpus module — none of the ask-nlp.mjs/ace.mjs/
|
|
25
|
+
// constructions.mjs/construction-banks.mjs/digest-bank.mjs family of stubs
|
|
26
|
+
// scripts/build-ask-bundle.mjs needs for chat.mjs is required here.
|
|
27
|
+
//
|
|
28
|
+
// One real gap remains: `createGraphService` pulls in source-slice.mjs,
|
|
29
|
+
// which imports `node:path` (join/resolve/sep) for span math, so a browser
|
|
30
|
+
// bundler with no Node shims will fail on that one specifier. Apply the
|
|
31
|
+
// same generic `stubNodeBuiltins` plugin scripts/lib/browser-bundle.mjs
|
|
32
|
+
// already exports for tmct's own bundles (or an equivalent node:path shim —
|
|
33
|
+
// e.g. esbuild's own `--define`/polyfill options, or a bundler that already
|
|
34
|
+
// ships a node:path shim, as most do). Re-run
|
|
35
|
+
// `node scripts/verify-ask-browser-entry.mjs` (clean) and
|
|
36
|
+
// `node scripts/verify-ask-browser-entry.mjs --with-stubs` (with the shim)
|
|
37
|
+
// after touching ask.mjs's or graph-service.mjs's import graph — if the
|
|
38
|
+
// no-stub run ever passes clean, or a new specifier shows up, update this
|
|
39
|
+
// comment to match what esbuild actually reports.
|
|
40
|
+
export { ask, resolveObject } from "../../domain/ask.mjs";
|
|
41
|
+
export { createGraphService } from "../../adapters/providers/graph-service.mjs";
|