@polycode-projects/the-mechanical-code-talker 2.5.2 → 2.5.4
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 -7
- package/bin/tmct.mjs +7 -4
- package/corpus/LICENSES.json +7 -0
- package/corpus/reference/LICENSE-NOTICE +36 -0
- package/corpus/reference/README.md +19 -0
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +367 -0
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-02.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-03.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-05.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-06.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-07.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-09.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-0f.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-12.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-13.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-14.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-15.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-16.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-18.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-19.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-1f.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-21.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-22.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-23.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-24.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-26.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-27.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-28.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-29.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2f.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-30.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-31.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-32.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-33.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-34.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-35.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-36.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-37.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-38.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-39.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3a.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3b.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3c.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3d.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3e.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-3f.jsonl.gz +0 -0
- package/package.json +5 -1
- package/src/adapters/corpus/reference-pack.mjs +113 -0
- package/src/adapters/memory/core.mjs +104 -10
- package/src/domain/ask.mjs +66 -9
- package/src/domain/cli-verbs.mjs +3 -3
- package/src/domain/codegraph.mjs +6 -0
- package/src/domain/dialogue-acts.mjs +160 -0
- package/src/domain/interpret/normalize.mjs +14 -2
- package/src/domain/interpret/strategies/keywords.mjs +10 -1
- package/src/domain/memory/trust.mjs +12 -0
- package/src/domain/reference-pack.mjs +72 -0
- package/src/domain/skos-view.mjs +111 -0
- package/src/domain/syllogise.mjs +647 -56
- package/src/services/chat.mjs +937 -142
- package/src/services/index.mjs +4 -1
- package/src/surfaces/web/chat-browser-entry.mjs +87 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +1187 -85
- package/src/tools/definitions.mjs +13 -0
- package/src/tools/handlers/index.mjs +2 -0
- package/src/tools/handlers/tmct-related.mjs +30 -0
package/src/services/chat.mjs
CHANGED
|
@@ -46,6 +46,10 @@ import { readConstructionFiles } from "../adapters/corpus/construction-banks.mjs
|
|
|
46
46
|
import { fuzzyMatchInSet, fuzzyBound } from "../domain/interpret/fuzzy.mjs";
|
|
47
47
|
import { loadLexicon, lookupNoun } from "../domain/grammar/lexicon.mjs";
|
|
48
48
|
import { pickPhrase } from "../domain/answer-variants.mjs";
|
|
49
|
+
import { REFERENCE_PACK_NAME, cleanMissReferenceTerm, renderReferenceAnswer, referenceProvenanceTag } from "../domain/reference-pack.mjs";
|
|
50
|
+
import { getReferencePackProvider } from "../adapters/corpus/reference-pack.mjs";
|
|
51
|
+
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
52
|
+
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
49
53
|
|
|
50
54
|
// Composition: the chat surface supplies the domain parser's default lemma/POS
|
|
51
55
|
// adapter (the browser bundle's ask-nlp stub carries no factory, so this is a
|
|
@@ -149,6 +153,47 @@ function deduceGoalFromParsed(parsed) {
|
|
|
149
153
|
return "understand a graph relationship";
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
// ---- dialogue acts (ISO 24617-2): a lookup over the lane decision ----
|
|
157
|
+
// A turn result may carry a `lane` string naming the router lane that
|
|
158
|
+
// answered it; withLast (and conversationalTurn's own mk) resolve it through
|
|
159
|
+
// dialogueActForLane and stamp `record.dialogueAct` — a fixed lookup over a
|
|
160
|
+
// decision already made, never a classifier. The honest miss is the row that
|
|
161
|
+
// must never drift: autoNegative in the autoFeedback dimension, tmct
|
|
162
|
+
// reporting its OWN processing failed, not a task answer.
|
|
163
|
+
|
|
164
|
+
/** Stamp the record with the lane's dialogue act (a no-op for an unmapped or
|
|
165
|
+
* absent lane) and put the label in the narrate trace. */
|
|
166
|
+
function attachDialogueAct(result, trace) {
|
|
167
|
+
const act = dialogueActForLane(result?.lane);
|
|
168
|
+
if (act && result?.record) {
|
|
169
|
+
result.record.dialogueAct = act;
|
|
170
|
+
note(trace, `dialogue act: ${act.act} (${act.dimension} dimension, ISO 24617-2)`);
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const PROPOSITIONAL_NODES = new Set(["boolean", "qualifier"]);
|
|
176
|
+
const PROPOSITIONAL_LEAD_RE = /^(?:is|are|am|was|were|does|do|did|can|could|will|would|shall|should|has|have|had|must)\b/i;
|
|
177
|
+
const SET_QUESTION_LEAD_RE = /^(?:what|which|who|whose|where|when|why|how|tell|show|list|define|describe|find|count|name)\b/i;
|
|
178
|
+
|
|
179
|
+
/** The dialogue-act lane for a runAsk turn. A recorded miss is ALWAYS the
|
|
180
|
+
* honest-miss lane, whatever the query shape — feedback about tmct's own
|
|
181
|
+
* processing. An answered turn is labelled by its question shape (yes/no
|
|
182
|
+
* vs set), from the parsed AST when one stood, else the lead word. Null
|
|
183
|
+
* when the turn is neither — the record simply carries no act. */
|
|
184
|
+
function askDialogueLane(parsed, query, recordMiss) {
|
|
185
|
+
if (recordMiss) return "honest-miss";
|
|
186
|
+
if (parsed) {
|
|
187
|
+
if (parsed.node) return PROPOSITIONAL_NODES.has(parsed.node) ? "ask-propositional" : "ask-set";
|
|
188
|
+
if (parsed.shape === "ask") return "ask-propositional";
|
|
189
|
+
if (parsed.shape) return "ask-set";
|
|
190
|
+
}
|
|
191
|
+
const q = String(query).trim();
|
|
192
|
+
if (PROPOSITIONAL_LEAD_RE.test(q)) return "ask-propositional";
|
|
193
|
+
if (SET_QUESTION_LEAD_RE.test(q)) return "ask-set";
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
152
197
|
/** Split the collected trace into buckets by its own leading category tag, so
|
|
153
198
|
* renderNarration can group like with like while the trace array itself stays
|
|
154
199
|
* a flat, chronological narrative — no structured side-channel to keep in
|
|
@@ -820,7 +865,17 @@ export function renderStats(graph) {
|
|
|
820
865
|
* which meant "who are you" always got the "here's what I can query" blurb and
|
|
821
866
|
* never a self-description — split so each gets the answer it actually asked for. */
|
|
822
867
|
const CAPABILITY_PHRASES = [
|
|
823
|
-
|
|
868
|
+
// The "so"/"uh"/"well" lead and the "for me"/"then" tail are pure discourse
|
|
869
|
+
// filler on the same question — tolerated so the casual forms land on the
|
|
870
|
+
// same orientation answer instead of the parse wall.
|
|
871
|
+
/^(?:(?:so|uh|um|erm|well|ok|okay),?\s+)*what can (?:you|u)(?:\s+(?:actually|really))? do(?:\s+for\s+(?:me|us))?(?:\s+(?:then|now|today|here))?\??$/i,
|
|
872
|
+
/^(?:(?:so|uh|um|erm|well|ok|okay),?\s+)*what do you(?:\s+(?:actually|really))? do(?:\s+for\s+(?:me|us))?(?:\s+(?:then|now|today|here))?\??$/i,
|
|
873
|
+
// "what have you got" / "what do you have" — the overview question in its
|
|
874
|
+
// casual spelling; without a frame, "got" parsed as a defines object.
|
|
875
|
+
/^what (?:have|do) (?:you|u) (?:got|have)(?:\s+for\s+me)?(?:\s+(?:here|then|today))?\??$/i,
|
|
876
|
+
// "tell me about this repo" — the orientation request by name; the
|
|
877
|
+
// vocabulary touch lane must not read "this repo" as a concept term.
|
|
878
|
+
/^tell me(?:\s+(?:something|more|a\s+little|a\s+bit))?\s+about (?:this|the|your)\s+(?:app|codebase|repo|repository|project|code)\??$/i,
|
|
824
879
|
// "what can you actually help with" — the natural pivot from small talk
|
|
825
880
|
// into a capability question; not covered by the "do" pair above since
|
|
826
881
|
// neither accepts "help (me)? with" as a synonym tail for "do".
|
|
@@ -1434,16 +1489,17 @@ function conversationalTurn(line, ctx) {
|
|
|
1434
1489
|
const raw = String(line);
|
|
1435
1490
|
const q = raw.toLowerCase().replace(/[.!?]+$/, "").replace(/\s+/g, " ").trim();
|
|
1436
1491
|
const t = (id, slots = {}) => tRender(ctx.templates, id, slots) ?? TEMPLATES_UNAVAILABLE;
|
|
1437
|
-
const mk = (answer, { end = false, miss = false, via = "template" } = {}) => {
|
|
1492
|
+
const mk = (answer, { end = false, miss = false, via = "template", lane = null } = {}) => {
|
|
1438
1493
|
const ts = new Date().toISOString();
|
|
1439
|
-
return {
|
|
1494
|
+
return attachDialogueAct({
|
|
1440
1495
|
answer,
|
|
1441
1496
|
logLines: [ts, `> ${raw}`, answer, ""],
|
|
1442
1497
|
record: { type: "turn", ts, query: raw, conversational: true, via, resolvedIds: [], answeredIds: [], miss },
|
|
1443
1498
|
focus: ctx.focus,
|
|
1444
1499
|
last: ctx.last, // a conversational turn never overwrites the last real answer
|
|
1500
|
+
lane,
|
|
1445
1501
|
...(end ? { end: true } : {}),
|
|
1446
|
-
};
|
|
1502
|
+
}, ctx.trace);
|
|
1447
1503
|
};
|
|
1448
1504
|
if (foldedBye(q)) {
|
|
1449
1505
|
note(ctx.trace, "goal: casual/social — ending the session (no graph intent)");
|
|
@@ -1465,7 +1521,7 @@ function conversationalTurn(line, ctx) {
|
|
|
1465
1521
|
if (signal === "thanks") {
|
|
1466
1522
|
note(ctx.trace, "goal: casual/social — acknowledgement, no graph intent");
|
|
1467
1523
|
note(ctx.trace, "lane: conversational — thanks (multi-clause phrase-shape match)");
|
|
1468
|
-
return mk(t(T_THANKS));
|
|
1524
|
+
return mk(t(T_THANKS), { lane: "thanks" });
|
|
1469
1525
|
}
|
|
1470
1526
|
}
|
|
1471
1527
|
if (WHY.has(q)) {
|
|
@@ -1494,7 +1550,7 @@ function conversationalTurn(line, ctx) {
|
|
|
1494
1550
|
// morning, hello there) keep their wording; only the default greeting swaps.
|
|
1495
1551
|
const id = (!T_GREETING_BY_PHRASE[greetHit] && noCodeGraph(ctx.graph)) ? T_GREETING_EMPTY : (T_GREETING_BY_PHRASE[greetHit] || T_GREETING);
|
|
1496
1552
|
note(ctx.trace, `pattern: template "${id}" (data/templates/responses.jsonl)`);
|
|
1497
|
-
return mk(t(id, { vocabHint: ctx.vocabHint }));
|
|
1553
|
+
return mk(t(id, { vocabHint: ctx.vocabHint }), { lane: "greeting" });
|
|
1498
1554
|
}
|
|
1499
1555
|
}
|
|
1500
1556
|
{
|
|
@@ -1503,23 +1559,23 @@ function conversationalTurn(line, ctx) {
|
|
|
1503
1559
|
note(ctx.trace, "goal: casual/social — acknowledgement, no graph intent");
|
|
1504
1560
|
note(ctx.trace, `lane: conversational — thanks/acknowledgement (${OK_ACK.has(q) ? "OK_ACK" : "THANKS"} closed set${thanksHit === q ? "" : ", elongation-collapsed"})`);
|
|
1505
1561
|
note(ctx.trace, `pattern: template "${T_THANKS}" (data/templates/responses.jsonl)`);
|
|
1506
|
-
return mk(t(T_THANKS));
|
|
1562
|
+
return mk(t(T_THANKS), { lane: "thanks" });
|
|
1507
1563
|
}
|
|
1508
1564
|
}
|
|
1509
1565
|
if (aiIdentityMatch(raw)) {
|
|
1510
1566
|
note(ctx.trace, "goal: identity — is tmct an AI/LLM (a very likely first question)");
|
|
1511
1567
|
note(ctx.trace, "lane: conversational — identity/AI (AI_IDENTITY_PHRASES closed set)");
|
|
1512
|
-
return mk(t(T_IDENTITY_NOT_LLM));
|
|
1568
|
+
return mk(t(T_IDENTITY_NOT_LLM), { lane: "help" });
|
|
1513
1569
|
}
|
|
1514
1570
|
if (FEELINGS_PHRASES.some((re) => re.test(raw))) {
|
|
1515
1571
|
note(ctx.trace, "goal: identity — does tmct have feelings/consciousness (small-talk persona finding)");
|
|
1516
1572
|
note(ctx.trace, "lane: conversational — identity/feelings (FEELINGS_PHRASES closed set)");
|
|
1517
|
-
return mk(t(T_IDENTITY_NO_FEELINGS));
|
|
1573
|
+
return mk(t(T_IDENTITY_NO_FEELINGS), { lane: "help" });
|
|
1518
1574
|
}
|
|
1519
1575
|
if (IDENTITY_PHRASES.some((re) => re.test(raw))) {
|
|
1520
1576
|
note(ctx.trace, "goal: identity — who/what tmct is, not a capability listing");
|
|
1521
1577
|
note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
|
|
1522
|
-
return mk(t(T_IDENTITY_SELF));
|
|
1578
|
+
return mk(t(T_IDENTITY_SELF), { lane: "help" });
|
|
1523
1579
|
}
|
|
1524
1580
|
// CAPABILITY_PHRASES' vague-opener entries are self-contained closed
|
|
1525
1581
|
// regexes, but a preamble ahead of one ("right, can you walk me through
|
|
@@ -1531,7 +1587,7 @@ function conversationalTurn(line, ctx) {
|
|
|
1531
1587
|
|| CAPABILITY_PHRASES.some((re) => re.test(applyPreambleFrames(raw))) || ORIENT_OPENERS.has(q)) {
|
|
1532
1588
|
note(ctx.trace, "goal: get oriented — what can tmct answer, how do I start");
|
|
1533
1589
|
note(ctx.trace, "lane: conversational — help/orientation (CAPABILITY_PHRASES/ORIENT_OPENERS / bare help / ?)");
|
|
1534
|
-
return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint));
|
|
1590
|
+
return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint), { lane: "help" });
|
|
1535
1591
|
}
|
|
1536
1592
|
// Fuzzy-typo fallback (A4): every exact/collapsed closed-set lookup above missed —
|
|
1537
1593
|
// try a bounded edit-distance match against the flattened conversational phrase
|
|
@@ -1544,11 +1600,11 @@ function conversationalTurn(line, ctx) {
|
|
|
1544
1600
|
note(ctx.trace, `goal: casual/social or orientation — fuzzy-typo match "${raw}" → "${fuzzyHit}"`);
|
|
1545
1601
|
note(ctx.trace, `lane: conversational — fuzzy typo tolerance (${bucket})`);
|
|
1546
1602
|
if (bucket === "bye") return mk(t(T_FAREWELL), { end: true });
|
|
1547
|
-
if (bucket === "thanks") return mk(t(T_THANKS));
|
|
1548
|
-
if (bucket === "identity") return mk(t(T_IDENTITY_SELF));
|
|
1549
|
-
if (bucket === "capability") return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint));
|
|
1603
|
+
if (bucket === "thanks") return mk(t(T_THANKS), { lane: "thanks" });
|
|
1604
|
+
if (bucket === "identity") return mk(t(T_IDENTITY_SELF), { lane: "help" });
|
|
1605
|
+
if (bucket === "capability") return mk(orientationAnswer(ctx.templates, ctx.graph, ctx.vocabHint), { lane: "help" });
|
|
1550
1606
|
const id = (!T_GREETING_BY_PHRASE[fuzzyHit] && noCodeGraph(ctx.graph)) ? T_GREETING_EMPTY : (T_GREETING_BY_PHRASE[fuzzyHit] || T_GREETING);
|
|
1551
|
-
return mk(t(id, { vocabHint: ctx.vocabHint }));
|
|
1607
|
+
return mk(t(id, { vocabHint: ctx.vocabHint }), { lane: "greeting" });
|
|
1552
1608
|
}
|
|
1553
1609
|
}
|
|
1554
1610
|
return null;
|
|
@@ -2141,7 +2197,17 @@ const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
|
2141
2197
|
// class term, the preposition and the target — the caller singularizes the
|
|
2142
2198
|
// term and normalizes "onto"→"on" before the same verbless resolution runs.
|
|
2143
2199
|
const GOAL_TEACH_IMPERATIVE_RE = new RegExp(
|
|
2144
|
-
`^(?:get|put|place)\\s+(?:(every|each|all|both)\\s+)?(?:the\\s+)?([\\w-]+?)\\s+(${PREP_SRC})\\s+([\\w-]+)[?.!]*$`, "i");
|
|
2200
|
+
`^(?:get|put|place|stack)\\s+(?:(every|each|all|both)\\s+)?(?:the\\s+)?([\\w-]+?)\\s+(${PREP_SRC})\\s+([\\w-]+)[?.!]*$`, "i");
|
|
2201
|
+
// The bare-NP voicing of the same goal ("the goal is all disks on peg-c") —
|
|
2202
|
+
// no "that", no "for", no verb. Folds into the verbless resolution exactly
|
|
2203
|
+
// like the imperative above (same captures, same singularize/prep fold).
|
|
2204
|
+
const GOAL_TEACH_NP_RE = new RegExp(
|
|
2205
|
+
`^the\\s+goal\\s+is\\s+(?:(every|each|all|both)\\s+)?(?:the\\s+)?([\\w-]+?)\\s+(${PREP_SRC})\\s+([\\w-]+)[?.!]*$`, "i");
|
|
2206
|
+
// A conjunction of goal atoms ("the goal is that disk-1 rests on peg-b and
|
|
2207
|
+
// disk-3 rests on peg-c") — each conjunct compiles to its own goal spec.
|
|
2208
|
+
const GOAL_TEACH_CONJUNCTION_RE = /^the\s+goal\s+is\s+that\s+(.+?)[.!?]*$/i;
|
|
2209
|
+
const GOAL_CONJUNCT_RE = new RegExp(
|
|
2210
|
+
`^(?:(every|each|all)\\s+)?([\\w-]+)\\s+([a-z]+s)\\s+(${PREP_SRC})\\s+([\\w-]+)$`, "i");
|
|
2145
2211
|
// Plan follow-up questions, answered off the ACTIVE plan state (never invented
|
|
2146
2212
|
// when no plan stands). "next move"/"continue" EXECUTE (PLAN_NEXT_RE above); these
|
|
2147
2213
|
// three only REPORT.
|
|
@@ -2158,6 +2224,11 @@ const BOARD_REVERSE_LOC_RE = new RegExp(
|
|
|
2158
2224
|
const BOARD_FORWARD_LOC_RE = new RegExp(
|
|
2159
2225
|
`^what\\s+(?:does|do|is)\\s+([\\w-]+)\\s+([a-z]+)(?:\\s+(${PREP_SRC}))?[?.!\\s]*$`, "i");
|
|
2160
2226
|
const BOARD_WHERE_RE = /^(?:where\s+is|where's)\s+([\w-]+)(?:\s+now)?[?.!\s]*$/i;
|
|
2227
|
+
// "where does disk-1 rest?" — the verbed spelling of the same board read;
|
|
2228
|
+
// without it the phrasing fell through to the code definition-locator.
|
|
2229
|
+
const BOARD_WHERE_DOES_RE = /^where\s+does\s+([\w-]+)\s+([a-z]+)(?:\s+now)?[?.!\s]*$/i;
|
|
2230
|
+
// "where is every disk" — the same read over every member of a taught class.
|
|
2231
|
+
const BOARD_WHERE_EVERY_RE = /^where\s+(?:is|are)\s+(?:every|each|all(?:\s+the)?)\s+([\w-]+?)[?.!\s]*$/i;
|
|
2161
2232
|
|
|
2162
2233
|
/** "<X> is <adjective>" — the property teach payload (wrapper-REQUIRED): a lazy
|
|
2163
2234
|
* subject and a single bare complement word. Never matches the "is a <noun>"
|
|
@@ -2693,6 +2764,11 @@ const GENERAL_VERB_DETERMINER_TEACH_RE = new RegExp(
|
|
|
2693
2764
|
`^(?:the\\s+|an?\\s+)([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+([a-z]+)\\s+(${PREP_SRC})\\s+(.+?)[.!?]*$`,
|
|
2694
2765
|
"i",
|
|
2695
2766
|
);
|
|
2767
|
+
/** The quantified possession teach ("every dog has fur", "all dogs have
|
|
2768
|
+
* tails") — the closed has/have verb pins the split the way the preposition
|
|
2769
|
+
* pins GENERAL_VERB_DETERMINER_TEACH_RE's, so a universal quantifier can
|
|
2770
|
+
* lead without any verb-position guessing. */
|
|
2771
|
+
const QUANTIFIED_HAS_TEACH_RE = /^(?:every|each|all)\s+([\w'-]+)\s+(?:has|have)\s+(.+?)[.!?]*$/i;
|
|
2696
2772
|
/** Verbs owned by an earlier, more specific recognizer in this lane — is/are
|
|
2697
2773
|
* (class-membership/property, above) and owns/maintains (ownership, above).
|
|
2698
2774
|
* generalVerbTeach declines outright on these so it can never race a more
|
|
@@ -2837,13 +2913,20 @@ async function generalVerbTeach(payload) {
|
|
|
2837
2913
|
// with the preposition pinning the verb; a sentence that frame can't pin
|
|
2838
2914
|
// declines here exactly as it always has.
|
|
2839
2915
|
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) {
|
|
2840
|
-
const
|
|
2841
|
-
if (
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2916
|
+
const quantHas = p.match(QUANTIFIED_HAS_TEACH_RE);
|
|
2917
|
+
if (quantHas) {
|
|
2918
|
+
subjectRaw = singularizeSurface(quantHas[1]);
|
|
2919
|
+
verbRaw = "has";
|
|
2920
|
+
objectRaw = quantHas[2];
|
|
2921
|
+
} else {
|
|
2922
|
+
const det = p.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
2923
|
+
if (!det) return null; // not a bare-name subject, and no preposition to pin the verb
|
|
2924
|
+
subjectRaw = det[1];
|
|
2925
|
+
verbRaw = det[2];
|
|
2926
|
+
// hand the preposition back to the shared fold below, so the minted
|
|
2927
|
+
// predicate comes from the one place that mints it
|
|
2928
|
+
objectRaw = `${det[3]} ${det[4]}`;
|
|
2929
|
+
}
|
|
2847
2930
|
}
|
|
2848
2931
|
const verb = verbRaw.toLowerCase();
|
|
2849
2932
|
if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
|
|
@@ -3462,7 +3545,22 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3462
3545
|
};
|
|
3463
3546
|
}
|
|
3464
3547
|
}
|
|
3465
|
-
//
|
|
3548
|
+
// Nothing stored to disagree with. The gate above is right to refuse
|
|
3549
|
+
// storing a bare negative with no positive behind it — but a subject
|
|
3550
|
+
// the store has never heard of fell PAST every teach lane onto the
|
|
3551
|
+
// code-question bootstrap message, which reads as a different product.
|
|
3552
|
+
// Decline by name instead, saying what would make the claim usable. A
|
|
3553
|
+
// KNOWN subject still falls through — the property/relation frames
|
|
3554
|
+
// downstream own those sentences.
|
|
3555
|
+
if (!priorRows.some((r) => r.subject === negSubject || r.object === negSubject)) {
|
|
3556
|
+
return {
|
|
3557
|
+
text: `I don't have anything about "${negSubject}" to attach "not ${negObject}" to — a bare negative with no positive to disagree with isn't stored. `
|
|
3558
|
+
+ `Teach me "${negSubject} is ${indefiniteArticleFor(negObject)} ${negObject}" first if that's the disagreement you mean, `
|
|
3559
|
+
+ `or "no ${negSubject} is a ${negObject}" to store the exclusion outright.`,
|
|
3560
|
+
via: "teach-miss", miss: true,
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
// a known subject with no stored positive — fall through (see the gate above).
|
|
3466
3564
|
}
|
|
3467
3565
|
|
|
3468
3566
|
// RETRACTION — "forget that X is a Y": wires the data-layer retraction
|
|
@@ -3480,9 +3578,9 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3480
3578
|
// claiming a specific, possibly-wrong reason.
|
|
3481
3579
|
if (retractForgetMatch) {
|
|
3482
3580
|
const { retractSubClassOf } = await import("../domain/syllogise.mjs");
|
|
3483
|
-
const { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts } = await import("../adapters/memory/core.mjs");
|
|
3581
|
+
const { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts, appendFacts: appendFactsForRetract } = await import("../adapters/memory/core.mjs");
|
|
3484
3582
|
const result = await retractSubClassOf(memoryDir, retractSubject, retractObject, {
|
|
3485
|
-
store: { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts },
|
|
3583
|
+
store: { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts, appendFacts: appendFactsForRetract },
|
|
3486
3584
|
});
|
|
3487
3585
|
if (result.found) {
|
|
3488
3586
|
const extra = result.count - 1; // beyond the target fact itself
|
|
@@ -3947,7 +4045,9 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3947
4045
|
// of THAT subject — the same word generalVerbTeach will store — and leave
|
|
3948
4046
|
// every other sentence reading its first word exactly as before.
|
|
3949
4047
|
const detLed = raw.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
3950
|
-
const
|
|
4048
|
+
const quantHasLed = detLed ? null : raw.match(QUANTIFIED_HAS_TEACH_RE);
|
|
4049
|
+
const subjectWord = detLed ? detLed[1].split(/\s+/).pop()
|
|
4050
|
+
: (quantHasLed ? singularizeSurface(quantHasLed[1]) : raw.match(/^([\w'-]+)/)?.[1]);
|
|
3951
4051
|
if (subjectWord && (await subjectIsNounOrPropn(subjectWord))) {
|
|
3952
4052
|
// A PLURAL explicit-capability surface ("wrens can hum") whose
|
|
3953
4053
|
// SINGULAR is a grounded term stores under the singular first — the
|
|
@@ -4285,15 +4385,48 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
4285
4385
|
// rich, module-grain overview with a thin one. Gated on the term looking
|
|
4286
4386
|
// like a path, so this widens the lane by exactly the shape that was
|
|
4287
4387
|
// missing and can never claim a vocabulary question.
|
|
4288
|
-
const
|
|
4289
|
-
const
|
|
4290
|
-
if (!
|
|
4291
|
-
if (/^(?:it|this|that|they|them)$/i.test(
|
|
4292
|
-
const
|
|
4293
|
-
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4296
|
-
|
|
4388
|
+
const identityMatch = m ? null : (q.match(MODULE_IDENTITY_RE)?.[1]?.trim() ?? null);
|
|
4389
|
+
const phrase = m ? m[1].trim() : (identityMatch ?? q);
|
|
4390
|
+
if (!phrase) return null;
|
|
4391
|
+
if (/^(?:it|this|that|they|them)$/i.test(phrase)) return null;
|
|
4392
|
+
const bare = phrase.replace(/^(?:the|a|an)\s+/i, "").trim();
|
|
4393
|
+
const phraseWords = bare.split(/\s+/);
|
|
4394
|
+
const pathTail = phraseWords[phraseWords.length - 1];
|
|
4395
|
+
// Only an ANCHORED phrasing (the orient/purpose match, or the "what is X"
|
|
4396
|
+
// identity match) may read a modifier-plus-path-tail phrase — the bare-q
|
|
4397
|
+
// fallback stays gated to a lone path shape, or any sentence that happens
|
|
4398
|
+
// to end in a module path would be claimed here.
|
|
4399
|
+
const tailLooksLikePath = !!(m || identityMatch) && phraseWords.length > 1 && MODULE_PATH_RE.test(pathTail);
|
|
4400
|
+
// The identity phrasing ("what is <term>") only ever claims a path-shaped
|
|
4401
|
+
// term — bare, or with modifier words ahead of a path-shaped tail; the
|
|
4402
|
+
// orient/purpose phrasings carry their own anchors.
|
|
4403
|
+
if (!m && !MODULE_PATH_RE.test(bare) && !tailLooksLikePath) return null;
|
|
4404
|
+
const ent = await resolveEntity(graph, m ? phrase : bare);
|
|
4405
|
+
if (ent) {
|
|
4406
|
+
const ind = graph.byId?.get?.(ent.id);
|
|
4407
|
+
if (!ind) return null;
|
|
4408
|
+
return { text: moduleOverviewText(graph, ind), via: "meta" };
|
|
4409
|
+
}
|
|
4410
|
+
// The stale-modifier residue guard the ask engine's resolver applies,
|
|
4411
|
+
// carried into this lane: modifier words the graph has no reading for never
|
|
4412
|
+
// resolve past silently ("the OLD store.mjs" is not store.mjs — the
|
|
4413
|
+
// modifier may be the question). Decline by name, pointing at the near
|
|
4414
|
+
// match, instead of falling to the bare wall.
|
|
4415
|
+
if (tailLooksLikePath) {
|
|
4416
|
+
const tailEnt = await resolveEntity(graph, pathTail);
|
|
4417
|
+
if (tailEnt) {
|
|
4418
|
+
const residue = phraseWords.slice(0, -1);
|
|
4419
|
+
const quoted = residue.map((w) => `"${w}"`).join(" and ");
|
|
4420
|
+
const names = residue.length === 1 ? "names" : "name";
|
|
4421
|
+
const past = residue.length === 1 ? "it" : "them";
|
|
4422
|
+
return {
|
|
4423
|
+
text: `no module matching "${bare}" found in the index. ${quoted} ${names} nothing here, and reading past ${past} would answer a different question. Did you mean ${tailEnt.label}?`,
|
|
4424
|
+
via: "meta",
|
|
4425
|
+
miss: true,
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
return null;
|
|
4297
4430
|
}
|
|
4298
4431
|
|
|
4299
4432
|
async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null, focus = null }) {
|
|
@@ -5042,6 +5175,21 @@ function isaPolarityReply(hit, negHit) {
|
|
|
5042
5175
|
return null;
|
|
5043
5176
|
}
|
|
5044
5177
|
|
|
5178
|
+
/** The verdict when a would-be "yes" (a stored fact or a proof chain) crosses
|
|
5179
|
+
* a stored disjointness on the same resolved chain: name both stored facts
|
|
5180
|
+
* and refuse to conclude. A proof is the strongest honesty claim this file
|
|
5181
|
+
* makes, and certifying one side of a stored contradiction would launder the
|
|
5182
|
+
* inconsistency as a derivation — so neither side wins, same discipline as
|
|
5183
|
+
* isaPolarityReply's both-sides verdict. */
|
|
5184
|
+
function isaInconsistencyRefusal(posFact, disjointFact) {
|
|
5185
|
+
const cite = (f) => `${factPhrase(f)}${f.provenance ? ` (source: ${f.provenance})` : ""}`;
|
|
5186
|
+
return {
|
|
5187
|
+
text: `you've told me both ${cite(posFact)} and ${cite(disjointFact)} — together those contradict, and I won't derive an answer from an inconsistency. `
|
|
5188
|
+
+ `To settle it, say "forget that ${posFact.subject} is ${indefiniteArticleFor(posFact.object)} ${posFact.object}".`,
|
|
5189
|
+
replace: true,
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5045
5193
|
/** PROOF-CHAIN RECEIPT — "renderable as a chain of thought in words": render
|
|
5046
5194
|
* an ordered list of
|
|
5047
5195
|
* premise Fact rows as one continuous argument — "cache is a kind of store;
|
|
@@ -5142,6 +5290,17 @@ function teachableSubjectOf(subject) {
|
|
|
5142
5290
|
}
|
|
5143
5291
|
}
|
|
5144
5292
|
|
|
5293
|
+
/** The teach-shaped restatement of a QUANTIFIED-PLURAL subject: "all dogs"
|
|
5294
|
+
* folds to "a dog", so an offered sentence stays grammatical and teachable —
|
|
5295
|
+
* echoing the quantifier into a singular frame produced "all dogs is
|
|
5296
|
+
* mortal". Any other subject keeps teachableSubjectOf's own reading. */
|
|
5297
|
+
function suggestibleSubjectPhrase(subject) {
|
|
5298
|
+
const m = String(subject || "").trim().match(/^(?:all|every|each|both|most|some)\s+([\w-]+)$/i);
|
|
5299
|
+
if (!m) return teachableSubjectOf(subject);
|
|
5300
|
+
const singular = teachableSubjectOf(singularizeSurface(m[1]));
|
|
5301
|
+
return `${indefiniteArticleFor(singular)} ${singular}`;
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5145
5304
|
/** A leading universal quantifier, which is scaffolding rather than part of a
|
|
5146
5305
|
* name. The teach frames strip exactly these before storing (UNKNOWN_SUBJECT_RE
|
|
5147
5306
|
* above carries the same set), so no fact is ever stored under a subject that
|
|
@@ -6149,8 +6308,19 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6149
6308
|
if (isa) {
|
|
6150
6309
|
const subj = factTermVariants(normFactTerm, isa[1]);
|
|
6151
6310
|
const obj = factTermVariants(normFactTerm, isa[2]);
|
|
6152
|
-
const isaRows = await
|
|
6311
|
+
const isaRows = await factRows(memoryDir, cache);
|
|
6153
6312
|
const onTerms = (f) => subj.has(f.subject) && obj.has(f.object);
|
|
6313
|
+
// A TAUGHT disjointness touching either asked term can flip or veto the
|
|
6314
|
+
// verdict — it is the negative side of the polarity when it links the
|
|
6315
|
+
// asked terms directly, and a positive whose ⊑-chain crosses one is a
|
|
6316
|
+
// stored contradiction, not a yes. That reasoning (and its refusal) lives
|
|
6317
|
+
// in the full is-a ladder, so this quick reader stands aside for it
|
|
6318
|
+
// rather than answering a yes it hasn't checked.
|
|
6319
|
+
const { DISJOINT_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
6320
|
+
const touchesAskedTerm = (f) => subj.has(f.subject) || subj.has(f.object) || obj.has(f.subject) || obj.has(f.object);
|
|
6321
|
+
if (isaRows.some((f) => f.predicate === DISJOINT_PREDICATE && isOperatorTaught(f) && touchesAskedTerm(f))) {
|
|
6322
|
+
return null;
|
|
6323
|
+
}
|
|
6154
6324
|
// A remembered NEGATIVE is read on the same terms as the positive — it
|
|
6155
6325
|
// carries its own predicate and so never reaches ISA_PREDICATES.
|
|
6156
6326
|
const reply = isaPolarityReply(
|
|
@@ -6193,19 +6363,32 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6193
6363
|
return capabilityBaseRateReply(can[1], can[2], facts);
|
|
6194
6364
|
}
|
|
6195
6365
|
|
|
6196
|
-
// (b2b) "does a dog have a tail" — yes iff a remembered
|
|
6197
|
-
// so: the forward yes/no mirror of WHAT_HAS_RE below, with the same
|
|
6366
|
+
// (b2b) "does a dog have a tail" — yes iff a remembered possession fact
|
|
6367
|
+
// says so: the forward yes/no mirror of WHAT_HAS_RE below, with the same
|
|
6198
6368
|
// single-hit lookup and "never a guessed no" discipline as CAN_ASK_RE
|
|
6199
6369
|
// above. Only diverts on a REAL hit, so a code-shaped "does app.mjs have
|
|
6200
|
-
// tests" (no
|
|
6370
|
+
// tests" (no possession fact) keeps whatever miss text already stands.
|
|
6371
|
+
// Both possession spellings are read (the corpus mints mgx:hasA, the teach
|
|
6372
|
+
// lane tmct:has), and the lookup lifts one taught ⊑-hop so "does rex have
|
|
6373
|
+
// fur" answers through "rex is a kind of dog; dog has fur", citing both.
|
|
6201
6374
|
const doesHave = q.match(DOES_HAVE_ASK_RE);
|
|
6202
6375
|
if (doesHave) {
|
|
6203
6376
|
const subj = factTermVariants(normFactTerm, doesHave[1]);
|
|
6204
6377
|
const obj = factTermVariants(normFactTerm, doesHave[2]);
|
|
6205
|
-
const
|
|
6206
|
-
|
|
6378
|
+
const HAS_PREDICATES = new Set(["mgx:hasA", "tmct:has"]);
|
|
6379
|
+
const facts = await memoryFacts(memoryDir);
|
|
6380
|
+
const hasHit = (subjectSet) => facts.find(
|
|
6381
|
+
(f) => HAS_PREDICATES.has(f.predicate) && subjectSet.has(f.subject) && obj.has(f.object),
|
|
6207
6382
|
);
|
|
6383
|
+
const hit = hasHit(subj);
|
|
6208
6384
|
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
6385
|
+
const isaStep = facts.find((f) => ISA_PREDICATES.has(f.predicate) && subj.has(f.subject));
|
|
6386
|
+
if (isaStep) {
|
|
6387
|
+
const lifted = hasHit(factTermVariants(normFactTerm, isaStep.object));
|
|
6388
|
+
if (lifted) {
|
|
6389
|
+
return { text: `yes — ${renderFactLine(isaStep)}; ${renderFactLine(lifted)}`, replace: true };
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6209
6392
|
return null;
|
|
6210
6393
|
}
|
|
6211
6394
|
|
|
@@ -6244,6 +6427,22 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6244
6427
|
}
|
|
6245
6428
|
const base = capabilityBaseRateReply(doAsk[2], doAsk[3], facts);
|
|
6246
6429
|
if (base) return base;
|
|
6430
|
+
// A subject NO fact row mentions on either side has nothing to answer
|
|
6431
|
+
// from at all — without this the turn fell through to the
|
|
6432
|
+
// conversational catch-all, which answered a question about penguins
|
|
6433
|
+
// with the identity blurb. Decline by name, with the round-trip teach
|
|
6434
|
+
// hint, and stay a miss. SINGLE-WORD subjects only: a multi-word
|
|
6435
|
+
// capture here is this loose shape misbinding a subject+verb ("does
|
|
6436
|
+
// margo eat ribs" reads [margo eat][ribs]), and a later reader owns
|
|
6437
|
+
// that sentence — the same keep-its-turn rule as the fall-through above.
|
|
6438
|
+
if (!/\s/.test(doAsk[2].trim()) && !facts.some((f) => subj.has(f.subject) || subj.has(f.object))) {
|
|
6439
|
+
const noun = singularizeSurface(teachableSubjectOf(doAsk[2]));
|
|
6440
|
+
return {
|
|
6441
|
+
text: `I can't confirm that — I don't know anything about "${doAsk[2]}" yet. Teach me "a ${noun} can ${doAsk[3]}" (or "a ${noun} cannot ${doAsk[3]}") and I'll remember it.`,
|
|
6442
|
+
replace: true,
|
|
6443
|
+
miss: true,
|
|
6444
|
+
};
|
|
6445
|
+
}
|
|
6247
6446
|
}
|
|
6248
6447
|
}
|
|
6249
6448
|
|
|
@@ -6551,7 +6750,11 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6551
6750
|
* pipeline even gets a look) would wrongly swallow that idiom as a
|
|
6552
6751
|
* vocabulary-term lookup for the literal term "in X". */
|
|
6553
6752
|
const WHAT_ELSE_IS_RE = /^what\s+else\s+(?:is|are)\s+(?!in\b|inside\b)(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
6554
|
-
const WHAT_ELSE_ABOUT_RE = /^what\s+else\s+(?:do\s+you\s+know\s+)?about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
6753
|
+
const WHAT_ELSE_ABOUT_RE = /^(?:what|anything)\s+else\s+(?:do\s+you\s+know\s+)?about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
6754
|
+
/** "what else can dogs do" — the capability spelling of the same beyond-the-
|
|
6755
|
+
* primary-answer question; the subject's remaining facts (capabilities
|
|
6756
|
+
* included) are the expansion it asks for. */
|
|
6757
|
+
const WHAT_ELSE_CAN_DO_RE = /^what\s+else\s+can\s+(?:an?\s+|the\s+)?(.+?)\s+do[?.!\s]*$/i;
|
|
6555
6758
|
/** The same question with its subject left implicit — "what else", "anything
|
|
6556
6759
|
* else", "what else do you know". A reader who has just been told about dogs
|
|
6557
6760
|
* and asks "what else" means "what else about dogs"; the subject is carried by
|
|
@@ -6581,7 +6784,7 @@ const WHAT_ELSE_BARE_RE = /^(?:(?:and|so|but|ok|okay|now|then)\s+)*(?:what\s+els
|
|
|
6581
6784
|
async function whatElseAnswer(memoryDir, query, last) {
|
|
6582
6785
|
if (!memoryDir) return null;
|
|
6583
6786
|
const q = String(query).trim();
|
|
6584
|
-
const m = q.match(WHAT_ELSE_IS_RE) || q.match(WHAT_ELSE_ABOUT_RE);
|
|
6787
|
+
const m = q.match(WHAT_ELSE_IS_RE) || q.match(WHAT_ELSE_ABOUT_RE) || q.match(WHAT_ELSE_CAN_DO_RE);
|
|
6585
6788
|
// A bare "what else" takes its subject from the standing referent — the same
|
|
6586
6789
|
// last-grounded-answer binding "can it bark" uses.
|
|
6587
6790
|
const bare = !m && WHAT_ELSE_BARE_RE.test(q);
|
|
@@ -7295,18 +7498,76 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7295
7498
|
const isaSubject = focusLabel && IS_ADJECTIVE_PRONOUN_RE.test(isaAsk[1].trim())
|
|
7296
7499
|
? focusLabel : isaAsk[1];
|
|
7297
7500
|
const subjCandidates = new Set(factTermVariants(normFactTerm, isaSubject));
|
|
7501
|
+
// REFLEXIVE subsumption — "is a dog a dog" holds by definition (⊑ is
|
|
7502
|
+
// reflexive, whatever the term); without this it fell to the can't-confirm
|
|
7503
|
+
// closer, which then offered to be taught "dog is a kind of dog".
|
|
7504
|
+
if ([...subjCandidates].some((s) => objVariants.has(s))) {
|
|
7505
|
+
const kindEcho = stripTrailingDiscourseTag(isaAsk[2]).trim();
|
|
7506
|
+
return {
|
|
7507
|
+
text: `yes — ${indefiniteArticleFor(kindEcho)} ${kindEcho} is ${indefiniteArticleFor(kindEcho)} ${kindEcho}, trivially: every kind is a kind of itself.`,
|
|
7508
|
+
replace: true,
|
|
7509
|
+
};
|
|
7510
|
+
}
|
|
7298
7511
|
const noun = await entityClassNoun(graph, isaSubject);
|
|
7299
7512
|
if (noun) for (const v of factTermVariants(normFactTerm, noun)) subjCandidates.add(v);
|
|
7513
|
+
const {
|
|
7514
|
+
findIsaChain, deriveDisjointViolations,
|
|
7515
|
+
SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE, DISJOINT_PREDICATE,
|
|
7516
|
+
} = await import("../domain/syllogise.mjs");
|
|
7517
|
+
const isTaught = isOperatorTaught;
|
|
7518
|
+
const chainSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE && isTaught(f));
|
|
7519
|
+
const chainTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE && isTaught(f));
|
|
7520
|
+
const chainSubClassEdges = chainSubClassRows.map((f) => [f.subject, f.object]);
|
|
7521
|
+
const chainTypeEdges = chainTypeRows.map((f) => [f.subject, f.object]);
|
|
7522
|
+
const mixedSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE);
|
|
7523
|
+
const mixedTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE);
|
|
7524
|
+
const mixedTypeEdges = mixedTypeRows.map((f) => [f.subject, f.object]);
|
|
7525
|
+
const mixedSubClassEdges = mixedSubClassRows.map((f) => [f.subject, f.object]);
|
|
7526
|
+
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
7527
|
+
const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
|
|
7528
|
+
// CAX-DW GATE, COMPUTED BEFORE ANY "YES" MAY RETURN: every taught
|
|
7529
|
+
// disjointness is lifted through the full ⊑-closure (subclass edges double
|
|
7530
|
+
// as type edges here, because an instance teach like "rex is a dog" stores
|
|
7531
|
+
// rdfs:subClassOf) and held against the asked conclusion. A "yes" whose
|
|
7532
|
+
// resolved chain crosses one of these would certify a stored
|
|
7533
|
+
// contradiction, so the gate runs ahead of the direct-fact verdict and
|
|
7534
|
+
// both proof chases below — never after them, where it can only lose.
|
|
7535
|
+
const disjointGateViolations = disjointRows.length
|
|
7536
|
+
? deriveDisjointViolations(
|
|
7537
|
+
mixedTypeEdges.concat(mixedSubClassEdges), mixedSubClassEdges, disjointEdges,
|
|
7538
|
+
{ budget: 20, focus: new Set([...subjCandidates, ...objVariants]) },
|
|
7539
|
+
)
|
|
7540
|
+
: [];
|
|
7541
|
+
const disjointRefusalFor = (subj) => {
|
|
7542
|
+
const v = disjointGateViolations.find((vv) => vv.subject === subj && objVariants.has(vv.object));
|
|
7543
|
+
if (!v) return null;
|
|
7544
|
+
const posFact = isa
|
|
7545
|
+
.filter((f) => objVariants.has(f.object) && (f.subject === v.viaClass || f.subject === v.subject))
|
|
7546
|
+
.sort(byTrust)[0];
|
|
7547
|
+
const disjointFact = disjointRows.find((f) => (f.subject === v.viaClass && f.object === v.object)
|
|
7548
|
+
|| (f.subject === v.object && f.object === v.viaClass));
|
|
7549
|
+
if (!posFact || !disjointFact) return null;
|
|
7550
|
+
return isaInconsistencyRefusal(posFact, disjointFact);
|
|
7551
|
+
};
|
|
7300
7552
|
const hit = isa
|
|
7301
7553
|
.filter((f) => subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7302
7554
|
.sort(byTrust)[0];
|
|
7303
7555
|
// A STORED NEGATIVE ("john is not a man") is a source disagreeing, so it is
|
|
7304
7556
|
// read on the same terms as the positive rather than losing to it by
|
|
7305
|
-
// default. It carries its own predicate and so never reaches `isa`.
|
|
7557
|
+
// default. It carries its own predicate and so never reaches `isa`. A
|
|
7558
|
+
// taught disjointness directly between the asked terms is the same
|
|
7559
|
+
// disagreement in owl:disjointWith spelling, so it reads as the negative
|
|
7560
|
+
// side on the same terms — ahead of every yes-chase, not after them.
|
|
7306
7561
|
const negHit = rows
|
|
7307
7562
|
.filter((f) => f.predicate === NEG_SUBCLASS_PREDICATE && subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7308
7563
|
.sort(byTrust)[0];
|
|
7309
|
-
const
|
|
7564
|
+
const directDisjoint = disjointRows.find((f) => (subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7565
|
+
|| (subjCandidates.has(f.object) && objVariants.has(f.subject)));
|
|
7566
|
+
if (hit && !negHit) {
|
|
7567
|
+
const chainRefusal = disjointRefusalFor(hit.subject);
|
|
7568
|
+
if (chainRefusal) return chainRefusal;
|
|
7569
|
+
}
|
|
7570
|
+
const polarityReply = isaPolarityReply(hit, negHit || directDisjoint);
|
|
7310
7571
|
if (polarityReply) return polarityReply;
|
|
7311
7572
|
// CLASS↔INSTANCE BRIDGE: when X resolves to a graph entity, its
|
|
7312
7573
|
// inherits chain's superclass LABELS are subject candidates too — a taught
|
|
@@ -7348,17 +7609,13 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7348
7609
|
// technically-true-per-ConceptNet "yes" that has nothing to do with
|
|
7349
7610
|
// what the OPERATOR taught; only operator/teach/entailed-sourced isa
|
|
7350
7611
|
// facts are chased, matching "TAUGHT" in the gap's own name.
|
|
7351
|
-
const { findIsaChain, SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
7352
|
-
const isTaught = isOperatorTaught;
|
|
7353
|
-
const chainSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE && isTaught(f));
|
|
7354
|
-
const chainTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE && isTaught(f));
|
|
7355
|
-
const chainSubClassEdges = chainSubClassRows.map((f) => [f.subject, f.object]);
|
|
7356
|
-
const chainTypeEdges = chainTypeRows.map((f) => [f.subject, f.object]);
|
|
7357
7612
|
const factForStep = (step) => (step.predicate === SC_PREDICATE ? chainSubClassRows : chainTypeRows)
|
|
7358
7613
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
7359
7614
|
for (const subj of subjCandidates) {
|
|
7360
7615
|
const chain = findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassEdges, { maxHops: 2 });
|
|
7361
7616
|
if (!chain) continue;
|
|
7617
|
+
const chainRefusal = disjointRefusalFor(subj);
|
|
7618
|
+
if (chainRefusal) return chainRefusal;
|
|
7362
7619
|
const premises = chain.map(factForStep);
|
|
7363
7620
|
if (premises.every(Boolean)) return { text: `yes — ${renderIsaChain(premises)}`, replace: true };
|
|
7364
7621
|
}
|
|
@@ -7373,15 +7630,13 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7373
7630
|
// every premise is cited with its own source, corpus ones included. The
|
|
7374
7631
|
// shared taught-only rows above stay untouched: the disjoint and
|
|
7375
7632
|
// someValuesFrom chases keep their original, narrower discipline.
|
|
7376
|
-
const mixedSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE);
|
|
7377
|
-
const mixedTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE);
|
|
7378
7633
|
const mixedFactForStep = (step) => (step.predicate === SC_PREDICATE ? mixedSubClassRows : mixedTypeRows)
|
|
7379
7634
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
7380
|
-
const mixedTypeEdges = mixedTypeRows.map((f) => [f.subject, f.object]);
|
|
7381
|
-
const mixedSubClassEdges = mixedSubClassRows.map((f) => [f.subject, f.object]);
|
|
7382
7635
|
for (const subj of subjCandidates) {
|
|
7383
7636
|
const chain = findIsaChain(subj, objVariants, mixedTypeEdges, mixedSubClassEdges, { maxHops: 2 });
|
|
7384
7637
|
if (!chain) continue;
|
|
7638
|
+
const chainRefusal = disjointRefusalFor(subj);
|
|
7639
|
+
if (chainRefusal) return chainRefusal;
|
|
7385
7640
|
const premises = chain.map(mixedFactForStep);
|
|
7386
7641
|
if (premises.every(Boolean) && premises.some(isTaught)) {
|
|
7387
7642
|
return { text: `yes — ${renderIsaChain(premises)}`, replace: true };
|
|
@@ -7397,8 +7652,6 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7397
7652
|
// answer "no" from absence-of-membership rather than decline; anything
|
|
7398
7653
|
// this chase can't connect through a stated disjointness falls through
|
|
7399
7654
|
// to the honest miss below, never a guessed "no".
|
|
7400
|
-
const { deriveDisjointViolations, DISJOINT_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
7401
|
-
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
7402
7655
|
// NEGATED membership — "is a dog not a cat". ISA_ASK_RE captures the
|
|
7403
7656
|
// subject as "dog not" (the "not" glues onto the subject because the
|
|
7404
7657
|
// article anchors the kind), so without this the negated question walks
|
|
@@ -7437,20 +7690,16 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7437
7690
|
};
|
|
7438
7691
|
}
|
|
7439
7692
|
if (disjointRows.length) {
|
|
7440
|
-
//
|
|
7441
|
-
//
|
|
7442
|
-
//
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|| (subjCandidates.has(f.object) && objVariants.has(f.subject)));
|
|
7447
|
-
if (directDisjoint) return { text: `no — ${renderFactLine(directDisjoint)}`, replace: true };
|
|
7448
|
-
const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
|
|
7449
|
-
const violations = deriveDisjointViolations(chainTypeEdges, chainSubClassEdges, disjointEdges, { budget: 10 });
|
|
7693
|
+
// Taught subclass edges double as type edges here, because an instance
|
|
7694
|
+
// teach ("felix is a cat") stores rdfs:subClassOf — without the fold the
|
|
7695
|
+
// instance form of the provable "no" never fired.
|
|
7696
|
+
const violations = deriveDisjointViolations(
|
|
7697
|
+
chainTypeEdges.concat(chainSubClassEdges), chainSubClassEdges, disjointEdges, { budget: 10 },
|
|
7698
|
+
);
|
|
7450
7699
|
for (const subj of subjCandidates) {
|
|
7451
7700
|
const v = violations.find((vv) => vv.subject === subj && objVariants.has(vv.object));
|
|
7452
7701
|
if (!v) continue;
|
|
7453
|
-
const typeFact = chainTypeRows.find((f) => f.subject === v.subject && f.object === v.viaType);
|
|
7702
|
+
const typeFact = chainTypeRows.concat(chainSubClassRows).find((f) => f.subject === v.subject && f.object === v.viaType);
|
|
7454
7703
|
const disjointFact = disjointRows.find((f) => (f.subject === v.viaClass && f.object === v.object)
|
|
7455
7704
|
|| (f.subject === v.object && f.object === v.viaClass));
|
|
7456
7705
|
const parts = [typeFact, disjointFact].filter(Boolean).map(renderFactLine);
|
|
@@ -7617,6 +7866,20 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7617
7866
|
miss: true, // still a MISS in the turn record — honest wording, not an answer
|
|
7618
7867
|
};
|
|
7619
7868
|
}
|
|
7869
|
+
// A stored CONVERSE ("every dog is a mammal" asked as "is a mammal a
|
|
7870
|
+
// dog") deserves better than the bare wall: name the direction that IS
|
|
7871
|
+
// known and why it doesn't answer. Still a miss, never a guessed "no" —
|
|
7872
|
+
// some mammals may well be dogs; the store just doesn't say.
|
|
7873
|
+
const converseHit = isa
|
|
7874
|
+
.filter((f) => subjCandidates.has(f.object) && objVariants.has(f.subject))
|
|
7875
|
+
.sort(byTrust)[0];
|
|
7876
|
+
if (converseHit) {
|
|
7877
|
+
return {
|
|
7878
|
+
text: `I can't confirm that — what I know runs the other way: ${renderFactLine(converseHit)}. A kind doesn't reverse. If it's true, teach me: "every ${subjectWord} is a ${kindWord}".`,
|
|
7879
|
+
replace: true,
|
|
7880
|
+
miss: true,
|
|
7881
|
+
};
|
|
7882
|
+
}
|
|
7620
7883
|
// Subject with NO isa facts: only divert when it's mentioned NOWHERE at
|
|
7621
7884
|
// all (no fact row on either side, no code entity by id OR class noun) —
|
|
7622
7885
|
// a subject known via OTHER predicates ("ahab is male") or the code graph
|
|
@@ -7945,7 +8208,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7945
8208
|
// originally-intended case here) still gets this receipt exactly as
|
|
7946
8209
|
// before, since envelope.parsed is null for those adjectives.
|
|
7947
8210
|
if (rows.some(subjectMatch) && !envelope?.parsed) {
|
|
7948
|
-
return { text: `I don't have a fact saying ${
|
|
8211
|
+
return { text: `I don't have a fact saying ${suggestibleSubjectPhrase(subject)} is ${adjective}.`, replace: true };
|
|
7949
8212
|
}
|
|
7950
8213
|
// Without this, "is the checkout flow
|
|
7951
8214
|
// deprecated" as a genuinely FIRST-EVER question about a subject tmct
|
|
@@ -8282,7 +8545,17 @@ function discourseRewrite(query, last) {
|
|
|
8282
8545
|
} else {
|
|
8283
8546
|
const sm = String(query).match(STACCATO_SWAP_RE);
|
|
8284
8547
|
const cand = sm?.[1]?.trim();
|
|
8285
|
-
if (!cand
|
|
8548
|
+
if (!cand) return null;
|
|
8549
|
+
// VOCABULARY STACCATO: "tell me about a dog" -> "and a cat". The article
|
|
8550
|
+
// plus a plain word is the gate on the NEW term (a bare "and stuff" never
|
|
8551
|
+
// matches), and the PRIOR turn must itself have been a vocabulary
|
|
8552
|
+
// question — a code drill-down chain keeps the code-ish NAME_TOKEN rule
|
|
8553
|
+
// below unchanged.
|
|
8554
|
+
const articled = cand.match(/^(?:an?|the)\s+([a-z][\w-]*)$/i);
|
|
8555
|
+
const prevWasVocab = last?.query
|
|
8556
|
+
&& (BARE_WHATIS_RE.test(String(last.query)) || vagueTouchTermOf(String(last.query)));
|
|
8557
|
+
if (articled && prevWasVocab) return `what is a ${singularizeSurface(articled[1])}`;
|
|
8558
|
+
if (!NAME_TOKEN_RE.test(cand)) return null;
|
|
8286
8559
|
newSubj = cand;
|
|
8287
8560
|
}
|
|
8288
8561
|
if (!last?.query) return null;
|
|
@@ -8556,6 +8829,46 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
|
|
|
8556
8829
|
return { text: `${def} (source: corpus/seon)`, term };
|
|
8557
8830
|
}
|
|
8558
8831
|
|
|
8832
|
+
// ---- learn-on-miss: the shipped reference pack behind the cleanest miss ----
|
|
8833
|
+
|
|
8834
|
+
/** The learn-on-miss gate, shared by the articled miss hook and the bare-form
|
|
8835
|
+
* fallback so the two can never disagree. Fires only on the CLEANEST miss: a
|
|
8836
|
+
* definition-shaped term the lexicon knows, resolving to no graph entity and
|
|
8837
|
+
* no remembered fact — then, and only then, the pack provider is consulted.
|
|
8838
|
+
* Null means the turn proceeds byte-identically to a pack-less run. */
|
|
8839
|
+
async function referencePackMissAnswer(term, { graph, memoryDir, lexicon, env, cache }) {
|
|
8840
|
+
if (!term || !memoryDir) return null;
|
|
8841
|
+
let key = null;
|
|
8842
|
+
try { key = cleanMissReferenceTerm(term, lexicon ?? undefined); } catch { key = null; }
|
|
8843
|
+
if (!key) return null;
|
|
8844
|
+
if (await resolveEntity(graph, term)) return null;
|
|
8845
|
+
let normFactTerm;
|
|
8846
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
8847
|
+
const variants = factTermVariants(normFactTerm, term);
|
|
8848
|
+
variants.add(key);
|
|
8849
|
+
const rows = await factRows(memoryDir, cache);
|
|
8850
|
+
if (rows.some((f) => variants.has(f.subject) || variants.has(f.object))) return null;
|
|
8851
|
+
let article = null;
|
|
8852
|
+
try { article = await getReferencePackProvider(env).lookup(key); } catch { article = null; }
|
|
8853
|
+
if (!article) return null;
|
|
8854
|
+
return { key, article, text: renderReferenceAnswer(key, article) };
|
|
8855
|
+
}
|
|
8856
|
+
|
|
8857
|
+
/** Store the article's first-sentence isa as a subClassOf fact carrying
|
|
8858
|
+
* reference provenance — AFTER the cited answer composed, and failure-
|
|
8859
|
+
* tolerated: the answer stands whether or not the fact lands. */
|
|
8860
|
+
async function appendReferenceIsaFact(memoryDir, key, article, cache) {
|
|
8861
|
+
if (!article?.isa) return;
|
|
8862
|
+
try {
|
|
8863
|
+
const { appendFact } = await import("../adapters/memory/core.mjs");
|
|
8864
|
+
await appendFact(memoryDir, {
|
|
8865
|
+
subject: key, predicate: "rdfs:subClassOf", object: article.isa,
|
|
8866
|
+
provenance: referenceProvenanceTag(article),
|
|
8867
|
+
});
|
|
8868
|
+
if (cache) cache.rows = null;
|
|
8869
|
+
} catch { /* tolerated — the cited answer is already composed */ }
|
|
8870
|
+
}
|
|
8871
|
+
|
|
8559
8872
|
/** The concept term a vague "what is a X" / "tell me about X" / "what does X mean" /
|
|
8560
8873
|
* "define X" asks about — metaTermOf's forms plus the "tell me about …" opener that
|
|
8561
8874
|
* the graph parser reads as a count. Null when the line isn't such a touch. The
|
|
@@ -8598,7 +8911,7 @@ function vagueTouchTermOf(query) {
|
|
|
8598
8911
|
q = q.replace(VAGUE_TOUCH_TEL_RE, "tell");
|
|
8599
8912
|
q = q.replace(VAGUE_TOUCH_ABUT_RE, "about");
|
|
8600
8913
|
q = applyPreambleFrames(q);
|
|
8601
|
-
const m = q.match(/^(?:kindly\s+)?tell me about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i)
|
|
8914
|
+
const m = q.match(/^(?:kindly\s+)?tell me (?:(?:something|a\s+little|a\s+bit|more)\s+)?about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i)
|
|
8602
8915
|
|| q.match(/^(?:(?:and|so|but|ok|okay|now|then|kindly)\s+)*what about\s+(?:an?\s+|the\s+)?(.+?)(?:\s+then|\s+though)?[?.!\s]*$/i)
|
|
8603
8916
|
// "explain X" — a bare "explain <term>" is at least as natural a vague touch as "tell
|
|
8604
8917
|
// me about X", but had no recognized shape at all: normalize.mjs's own
|
|
@@ -8687,7 +9000,7 @@ function relationTermOf(query, envelope) {
|
|
|
8687
9000
|
* "tell me" branch and a single external \s+ would double-count the
|
|
8688
9001
|
* separator when "about" fires. */
|
|
8689
9002
|
const DESCRIBE_WRAPPER_RE =
|
|
8690
|
-
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:more\s+)?(?:about\s+)?|describe\s+|what(?:'s|\s+is)?\s+about\s+)(.+?)(?:\s+for\s+me)?(?:\s+please)?\s*\??$/i;
|
|
9003
|
+
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?(?:tell\s+me\s+(?:(?:more|something|a\s+little|a\s+bit)\s+)?(?:about\s+)?|describe\s+|what(?:'s|\s+is)?\s+about\s+)(.+?)(?:\s+for\s+me)?(?:\s+please)?\s*\??$/i;
|
|
8691
9004
|
|
|
8692
9005
|
/** Bare focus pronouns this lane resolves against the STANDING focus —
|
|
8693
9006
|
* "describe that" / "tell me about it" after a prior turn set the focus.
|
|
@@ -9147,15 +9460,28 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9147
9460
|
// the frame below once the store names the verb — same spec, same
|
|
9148
9461
|
// confirmation, same fold as its verbed twin.
|
|
9149
9462
|
let verblessGoal = goalMatch ? null : q.match(GOAL_TEACH_VERBLESS_RE);
|
|
9150
|
-
//
|
|
9151
|
-
//
|
|
9463
|
+
// A conjunction of goal atoms ("the goal is that disk-1 rests on peg-b and
|
|
9464
|
+
// disk-3 rests on peg-c") — every conjunct must compile, else the single-goal
|
|
9465
|
+
// frames (and their honest declines) keep their turn.
|
|
9466
|
+
let conjunctMatches = null;
|
|
9467
|
+
if (!goalMatch && !verblessGoal) {
|
|
9468
|
+
const conj = q.match(GOAL_TEACH_CONJUNCTION_RE);
|
|
9469
|
+
if (conj && /\s+and\s+/i.test(conj[1])) {
|
|
9470
|
+
const parts = conj[1].split(/\s+and\s+/i).map((p) => p.trim());
|
|
9471
|
+
const matched = parts.map((p) => p.match(GOAL_CONJUNCT_RE));
|
|
9472
|
+
if (parts.length > 1 && matched.every(Boolean)) conjunctMatches = matched;
|
|
9473
|
+
}
|
|
9474
|
+
}
|
|
9475
|
+
// The imperative voicing ("get all the disks onto peg-c") and the bare-NP
|
|
9476
|
+
// voicing ("the goal is all disks on peg-c") fold into the same verbless
|
|
9477
|
+
// resolution: singularize the class term ("disks"→"disk"), read the
|
|
9152
9478
|
// universal off the quantifier, and normalize the motion preposition to the
|
|
9153
9479
|
// static one a location fact is stored under ("onto"→"on").
|
|
9154
|
-
if (!goalMatch && !verblessGoal) {
|
|
9155
|
-
const
|
|
9156
|
-
if (
|
|
9157
|
-
const prep = { onto: "on", into: "in", upon: "on" }[
|
|
9158
|
-
verblessGoal = [
|
|
9480
|
+
if (!goalMatch && !verblessGoal && !conjunctMatches) {
|
|
9481
|
+
const bare = q.match(GOAL_TEACH_NP_RE) || q.match(GOAL_TEACH_IMPERATIVE_RE);
|
|
9482
|
+
if (bare) {
|
|
9483
|
+
const prep = { onto: "on", into: "in", upon: "on" }[bare[3].toLowerCase()] ?? bare[3].toLowerCase();
|
|
9484
|
+
verblessGoal = [bare[0], bare[1] ? "every" : "", singularizeSurface(bare[2]), prep, bare[4]];
|
|
9159
9485
|
}
|
|
9160
9486
|
}
|
|
9161
9487
|
if (verblessGoal) {
|
|
@@ -9178,29 +9504,35 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9178
9504
|
}
|
|
9179
9505
|
goalMatch = [verblessGoal[0], verblessGoal[1], verblessGoal[2], verbs[0], verblessGoal[3], verblessGoal[4]];
|
|
9180
9506
|
}
|
|
9181
|
-
if (goalMatch) {
|
|
9507
|
+
if (goalMatch || conjunctMatches) {
|
|
9182
9508
|
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
9183
|
-
const
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9509
|
+
const items = conjunctMatches ?? [goalMatch];
|
|
9510
|
+
const specs = [];
|
|
9511
|
+
const tails = [];
|
|
9512
|
+
for (const m of items) {
|
|
9513
|
+
const verb = await verbLemma(m[3]);
|
|
9514
|
+
if (!verb) {
|
|
9515
|
+
return {
|
|
9516
|
+
text: `I can't reduce "${m[3]}" to a verb for that goal — try the plain form (e.g. "rests").`,
|
|
9517
|
+
via: "plan", deduced: "record the goal state for a later plan", note: "GOAL frame — verb lemma unavailable, honest decline",
|
|
9518
|
+
};
|
|
9519
|
+
}
|
|
9520
|
+
specs.push({
|
|
9521
|
+
universal: !!m[1],
|
|
9522
|
+
term: normFactTerm(m[2]),
|
|
9523
|
+
predicate: `${verb}-${m[4].toLowerCase()}`,
|
|
9524
|
+
object: normFactTerm(m[5]),
|
|
9525
|
+
});
|
|
9526
|
+
// A conjunct restates itself; the that-form keeps its own words; the
|
|
9527
|
+
// infinitive/verbless voicings restate as the that-form, so the goal
|
|
9528
|
+
// check's own "done — …" line and the confirmation read identically.
|
|
9529
|
+
tails.push(conjunctMatches
|
|
9530
|
+
? `${m[1] ? `${m[1].toLowerCase()} ` : ""}${m[2].toLowerCase()} ${m[3].toLowerCase()} ${m[4].toLowerCase()} ${m[5].toLowerCase()}`
|
|
9531
|
+
: (thatGoal
|
|
9532
|
+
? q.replace(/^the\s+goal\s+is\s+that\s+/i, "").replace(/[.!?]+$/, "")
|
|
9533
|
+
: `${m[1] ? `${m[1].toLowerCase()} ` : ""}${m[2].toLowerCase()} ${verb}s ${m[4].toLowerCase()} ${m[5].toLowerCase()}`));
|
|
9189
9534
|
}
|
|
9190
|
-
const spec = {
|
|
9191
|
-
universal: !!goalMatch[1],
|
|
9192
|
-
term: normFactTerm(goalMatch[2]),
|
|
9193
|
-
predicate: `${verb}-${goalMatch[4].toLowerCase()}`,
|
|
9194
|
-
object: normFactTerm(goalMatch[5]),
|
|
9195
|
-
};
|
|
9196
|
-
const tail = thatGoal
|
|
9197
|
-
? q.replace(/^the\s+goal\s+is\s+that\s+/i, "").replace(/[.!?]+$/, "")
|
|
9198
|
-
// The infinitive voicing restates as the that-form, so the goal check's
|
|
9199
|
-
// own "done — …" line and the confirmation read identically either way.
|
|
9200
|
-
: `${goalMatch[1] ? `${goalMatch[1].toLowerCase()} ` : ""}${goalMatch[2].toLowerCase()} ${verb}s ${goalMatch[4].toLowerCase()} ${goalMatch[5].toLowerCase()}`;
|
|
9201
9535
|
const prev = planHolder.state && Array.isArray(planHolder.state.goals) && !planHolder.state.done ? planHolder.state : null;
|
|
9202
|
-
const heldGoals = prev?.goals ?? [];
|
|
9203
|
-
const heldTexts = prev?.goalTexts ?? [];
|
|
9204
9536
|
// Restating a goal you already set is one goal, not two. The spec is four
|
|
9205
9537
|
// normalized scalars, so the same goal in either voicing ("the goal is
|
|
9206
9538
|
// that …" / "the goal is to …") compiles to the identical object and a
|
|
@@ -9211,19 +9543,26 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9211
9543
|
// goals and goalTexts move in LOCKSTEP — "solve it" joins goalTexts by
|
|
9212
9544
|
// index to describe the specs it compiled, so dropping one without the
|
|
9213
9545
|
// other misaligns the plan's own account of what it is solving for.
|
|
9214
|
-
|
|
9546
|
+
let heldGoals = prev?.goals ?? [];
|
|
9547
|
+
let heldTexts = prev?.goalTexts ?? [];
|
|
9548
|
+
let added = 0;
|
|
9549
|
+
for (let i = 0; i < specs.length; i += 1) {
|
|
9550
|
+
if (heldGoals.some((g) => sameGoalSpec(g, specs[i]))) continue;
|
|
9551
|
+
heldGoals = [...heldGoals, specs[i]];
|
|
9552
|
+
heldTexts = [...heldTexts, tails[i]];
|
|
9553
|
+
added += 1;
|
|
9554
|
+
}
|
|
9215
9555
|
planHolder.state = {
|
|
9216
|
-
goals:
|
|
9217
|
-
goalTexts: alreadyHeld ? heldTexts : [...heldTexts, tail],
|
|
9556
|
+
goals: heldGoals, goalTexts: heldTexts,
|
|
9218
9557
|
actions: null, states: null, stepGoals: null, cursor: 0, done: false,
|
|
9219
9558
|
};
|
|
9220
|
-
const n =
|
|
9559
|
+
const n = heldGoals.length;
|
|
9221
9560
|
return {
|
|
9222
|
-
text: `${
|
|
9223
|
-
via: "plan", deduced: "record the goal state for a later plan",
|
|
9224
|
-
note:
|
|
9225
|
-
?
|
|
9226
|
-
: "GOAL frame — goal spec
|
|
9561
|
+
text: `${added ? "noted" : "already noted"} — the goal is that ${tails.join(" and ")}.${n > 1 ? ` (${n} goals held)` : ""} Say "solve it" when the state is taught.`,
|
|
9562
|
+
via: "plan", lane: "goal", deduced: "record the goal state for a later plan",
|
|
9563
|
+
note: added
|
|
9564
|
+
? `GOAL frame — ${added === 1 ? "goal spec" : `${added} goal specs`} accumulated on the session plan slot`
|
|
9565
|
+
: "GOAL frame — the same goal spec was already held, so it folded onto the existing one",
|
|
9227
9566
|
};
|
|
9228
9567
|
}
|
|
9229
9568
|
|
|
@@ -9282,6 +9621,24 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9282
9621
|
}
|
|
9283
9622
|
const goals = planHolder.state.goals;
|
|
9284
9623
|
const goalText = planHolder.state.goalTexts.join("; ");
|
|
9624
|
+
// A goal naming a term the board and the taught classes have never heard of
|
|
9625
|
+
// ("peg-z") can never be reached — decline by name BEFORE the search, so an
|
|
9626
|
+
// unknown token is a named miss rather than a full-depth search burn.
|
|
9627
|
+
const knownTerms = new Set([
|
|
9628
|
+
...Object.keys(domain.classMembers || {}),
|
|
9629
|
+
...Object.values(domain.classMembers || {}).flat(),
|
|
9630
|
+
...state.flatMap((r) => [r.subject, r.object]),
|
|
9631
|
+
]);
|
|
9632
|
+
const unknownGoalTerms = [...new Set(goals.flatMap((g) => [g.term, g.object]))]
|
|
9633
|
+
.filter((t) => t && !knownTerms.has(t));
|
|
9634
|
+
if (unknownGoalTerms.length) {
|
|
9635
|
+
const quoted = unknownGoalTerms.map((t) => `"${t}"`).join(" and ");
|
|
9636
|
+
return {
|
|
9637
|
+
text: `I can't plan toward that goal — ${quoted} name${unknownGoalTerms.length === 1 ? "s" : ""} nothing the board or the taught classes know. Teach ${unknownGoalTerms.length === 1 ? "it" : "them"} first (e.g. "${unknownGoalTerms[0]} is a peg").`,
|
|
9638
|
+
via: "plan", deduced: "plan a move sequence (unknown goal term)",
|
|
9639
|
+
note: "plan lane — honest decline: the goal names an untaught term, search never started",
|
|
9640
|
+
};
|
|
9641
|
+
}
|
|
9285
9642
|
let isGoal;
|
|
9286
9643
|
try {
|
|
9287
9644
|
isGoal = compileGoal(goals, domain);
|
|
@@ -9327,14 +9684,22 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9327
9684
|
};
|
|
9328
9685
|
const ruleNames = [...new Set(domain.actions.map((a) => a.name))].join('", "');
|
|
9329
9686
|
const moveLines = actions.map((a, i) => ` ${i + 1}. ${a.label}`);
|
|
9687
|
+
// A piece the goal reaches for with no taught position is an ASSUMPTION the
|
|
9688
|
+
// plan silently makes (it reads the board as taught, without that piece) —
|
|
9689
|
+
// said out loud with the plan rather than left implicit.
|
|
9690
|
+
const goalPieces = [...new Set(goals.flatMap((g) => (g.universal ? (domain.classMembers?.[g.term] || []) : [g.term])))];
|
|
9691
|
+
const unplacedPieces = goalPieces.filter((p) => !state.some((r) => r.subject === p));
|
|
9692
|
+
const assumptionNote = unplacedPieces.length
|
|
9693
|
+
? `\n\nnote — ${unplacedPieces.join(" and ")} ha${unplacedPieces.length === 1 ? "s" : "ve"} no taught position, so this plan reads the board without ${unplacedPieces.length === 1 ? "it" : "them"}. Teach the missing position(s) and solve again if that's wrong.`
|
|
9694
|
+
: "";
|
|
9330
9695
|
const text = n === 0
|
|
9331
|
-
? `the goal already holds — nothing to do
|
|
9696
|
+
? `the goal already holds — nothing to do.${assumptionNote}`
|
|
9332
9697
|
: `plan found — ${n} move${n === 1 ? "" : "s"} (shortest):\n${moveLines.join("\n")}\n\n` +
|
|
9333
9698
|
`because — you taught me the "${ruleNames}" rule${domain.actions.length === 1 ? "" : "s"}` +
|
|
9334
9699
|
`${ordering.length ? ` and ${ordering.length} ordering fact${ordering.length === 1 ? "" : "s"}` : ""}. ` +
|
|
9335
|
-
`Say "next" to make move 1, or ask "what moves are legal now"
|
|
9700
|
+
`Say "next" to make move 1, or ask "what moves are legal now".${assumptionNote}`;
|
|
9336
9701
|
return {
|
|
9337
|
-
text, via: "plan",
|
|
9702
|
+
text, via: "plan", lane: "imperative",
|
|
9338
9703
|
deduced: `plan a move sequence from the current state to the goal (${n} move${n === 1 ? "" : "s"})`,
|
|
9339
9704
|
note: "plan lane — compileDomain + findActionPath over the taught rules; plan held on the session slot",
|
|
9340
9705
|
plan,
|
|
@@ -9419,8 +9784,10 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder }) {
|
|
|
9419
9784
|
const clear = q.match(IS_CLEAR_RE);
|
|
9420
9785
|
const rev = clear ? null : q.match(BOARD_REVERSE_LOC_RE);
|
|
9421
9786
|
const fwd = clear || rev ? null : q.match(BOARD_FORWARD_LOC_RE);
|
|
9422
|
-
const
|
|
9423
|
-
|
|
9787
|
+
const whereEvery = clear || rev || fwd ? null : q.match(BOARD_WHERE_EVERY_RE);
|
|
9788
|
+
const where = clear || rev || fwd || whereEvery ? null
|
|
9789
|
+
: (q.match(BOARD_WHERE_RE) || q.match(BOARD_WHERE_DOES_RE));
|
|
9790
|
+
if (!clear && !rev && !fwd && !where && !whereEvery) return null;
|
|
9424
9791
|
if (!memoryDir) return null;
|
|
9425
9792
|
|
|
9426
9793
|
let ctx;
|
|
@@ -9438,6 +9805,20 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder }) {
|
|
|
9438
9805
|
? { text: `no — ${x} is not clear: ${on.map(factPhrase).join("; ")}.`, deduced: "check whether a board piece is clear", note: "BOARD — clearness derived from the current board (a piece rests on it)" }
|
|
9439
9806
|
: { text: `yes — ${x} is clear: nothing rests on it on the current board.`, deduced: "check whether a board piece is clear", note: "BOARD — clearness derived from the current board (nothing rests on it)" };
|
|
9440
9807
|
}
|
|
9808
|
+
if (whereEvery) {
|
|
9809
|
+
const cls = normFactTerm(singularizeSurface(whereEvery[1]));
|
|
9810
|
+
const members = domain.classMembers?.[cls] || [];
|
|
9811
|
+
if (!members.length) return null; // not a taught class — the ordinary readers decide
|
|
9812
|
+
const lines = members.map((mbr) => {
|
|
9813
|
+
const rows = state.filter((r) => r.subject === mbr);
|
|
9814
|
+
return rows.length ? rows.map(factPhrase).join("; ") : `nothing on the current board says where ${mbr} is`;
|
|
9815
|
+
});
|
|
9816
|
+
return {
|
|
9817
|
+
text: lines.join("\n"),
|
|
9818
|
+
deduced: "read the current board (where every member of a class is)",
|
|
9819
|
+
note: "BOARD — forward locative for every member of the taught class",
|
|
9820
|
+
};
|
|
9821
|
+
}
|
|
9441
9822
|
if (where || fwd) {
|
|
9442
9823
|
const x = normFactTerm((where ?? fwd)[1]);
|
|
9443
9824
|
if (!individuals.has(x)) return null;
|
|
@@ -9687,6 +10068,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9687
10068
|
// facts/recall (a fact EXTENDS a non-miss schema hit too — NOT miss-gated),
|
|
9688
10069
|
// (4) TEACH lane (would-miss), (5) the short tailored miss (would-miss).
|
|
9689
10070
|
let handled = false;
|
|
10071
|
+
// The dialogue-act lane, when a lane below knows better than the final
|
|
10072
|
+
// question-shape lookup (a plan frame is a request/instruct, a stored
|
|
10073
|
+
// teach is an inform, whatever the surface punctuation looked like).
|
|
10074
|
+
let dialogueLaneOverride = null;
|
|
9690
10075
|
// (0) "what else is X" — recognized off the RAW query text, before every
|
|
9691
10076
|
// other lane below (all of which read `envelope`, already relaxed/reparsed
|
|
9692
10077
|
// by ask()'s noise-strip cascade, which silently drops "else"). via is set
|
|
@@ -9710,7 +10095,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9710
10095
|
if (miss) {
|
|
9711
10096
|
const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint, focus });
|
|
9712
10097
|
if (meta) {
|
|
9713
|
-
|
|
10098
|
+
// A lane may answer with a better-worded decline (the module-orient
|
|
10099
|
+
// residue guard) — still a miss in the turn record, like the isa
|
|
10100
|
+
// ladder's own closers.
|
|
10101
|
+
answer = meta.text; via = meta.via; recordMiss = meta.miss ?? false; handled = true;
|
|
9714
10102
|
note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
|
|
9715
10103
|
}
|
|
9716
10104
|
}
|
|
@@ -9723,6 +10111,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9723
10111
|
const planLane = await planLaneAnswer(query, { memoryDir, planHolder, sessionId });
|
|
9724
10112
|
if (planLane) {
|
|
9725
10113
|
answer = planLane.text; via = planLane.via; recordMiss = false; handled = true;
|
|
10114
|
+
if (planLane.lane) dialogueLaneOverride = planLane.lane;
|
|
9726
10115
|
if (planLane.plan) planResult = planLane.plan;
|
|
9727
10116
|
if (planLane.deduced) {
|
|
9728
10117
|
deduced = planLane.deduced;
|
|
@@ -9916,6 +10305,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9916
10305
|
const def = await curatedDefinitionAnswer(gateQuery, envelope, { memoryDir, lexicon });
|
|
9917
10306
|
if (def) bareMetaHit = { text: def.text, replace: true };
|
|
9918
10307
|
}
|
|
10308
|
+
// The reference pack's bare-form fallback, beside the curated one and
|
|
10309
|
+
// under the IDENTICAL clean-miss gate the articled hook (4h) applies —
|
|
10310
|
+
// "what is otter" reaches the pack exactly as "what is an otter" does.
|
|
10311
|
+
if (!bareMetaHit) {
|
|
10312
|
+
const refTerm = metaTermOf(gateQuery, envelope);
|
|
10313
|
+
const ref = refTerm ? await referencePackMissAnswer(refTerm, { graph, memoryDir, lexicon, env, cache }) : null;
|
|
10314
|
+
if (ref) bareMetaHit = { text: ref.text, replace: true, reference: ref };
|
|
10315
|
+
}
|
|
9919
10316
|
}
|
|
9920
10317
|
// A bare "what is X" naming a REAL code-graph entity (not a taught fact,
|
|
9921
10318
|
// not a curated corpus term) needs the SAME race fixed too.
|
|
@@ -9951,7 +10348,18 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9951
10348
|
if (fallback) bareMetaHit = { text: fallback.text, replace: true };
|
|
9952
10349
|
}
|
|
9953
10350
|
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
9954
|
-
if (bareMetaHit) {
|
|
10351
|
+
if (bareMetaHit?.reference) {
|
|
10352
|
+
// The bare-form reference hit mirrors (4h): the cited answer replaces the
|
|
10353
|
+
// miss, the turn is no longer recorded as one, and the article's isa is
|
|
10354
|
+
// stored after the answer composes.
|
|
10355
|
+
answer = bareMetaHit.text;
|
|
10356
|
+
via = "reference";
|
|
10357
|
+
recordMiss = false;
|
|
10358
|
+
handled = true;
|
|
10359
|
+
note(trace, "lane: (2b) REFERENCE PACK — a bare \"what is X\" clean miss answered from the shipped reference pack, cited");
|
|
10360
|
+
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${bareMetaHit.reference.article.title}" (revid ${bareMetaHit.reference.article.revid})`);
|
|
10361
|
+
await appendReferenceIsaFact(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache);
|
|
10362
|
+
} else if (bareMetaHit) {
|
|
9955
10363
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
9956
10364
|
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
9957
10365
|
// honest miss in better words — the turn record keeps miss=true and via
|
|
@@ -10161,6 +10569,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10161
10569
|
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache });
|
|
10162
10570
|
if (taught) {
|
|
10163
10571
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
10572
|
+
if (!taught.miss) dialogueLaneOverride = "teach";
|
|
10164
10573
|
note(trace, `lane: (4) TEACH — TEACH_RE/OWNS_TEACH_RE/BARE_DECLARATIVE_RE matched, ${taught.miss ? "but the payload could not be stored" : "reified into .tmct/memory"}`);
|
|
10165
10574
|
// `deduced` was computed straight off envelope.parsed alone, but the
|
|
10166
10575
|
// structural grammar has no business parsing a teach-shaped sentence at
|
|
@@ -10305,6 +10714,24 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10305
10714
|
note(trace, `lane: (4g) FUZZY-VERB DECLINE — "${from}" only became a verb through the edit-distance repair tier ("${to}"), and the two words are different verbs, so the repaired sentence's graph answer is dropped rather than shown as an answer to what was typed`);
|
|
10306
10715
|
}
|
|
10307
10716
|
}
|
|
10717
|
+
// (4h) REFERENCE PACK — the cleanest miss consults the shipped reference
|
|
10718
|
+
// pack: a definition-shaped term the lexicon knows, no graph entity, no
|
|
10719
|
+
// remembered fact. A hit answers with the article's summary, always cited;
|
|
10720
|
+
// a null from any gate leaves the turn byte-identical. After the answer
|
|
10721
|
+
// composes, the article's first-sentence isa is stored as a subClassOf fact
|
|
10722
|
+
// with reference provenance, so the NEXT ask answers from memory.
|
|
10723
|
+
if (miss && recordMiss && via === "composed" && memoryDir) {
|
|
10724
|
+
const refTerm = metaTermOf(query, envelope);
|
|
10725
|
+
const ref = refTerm ? await referencePackMissAnswer(refTerm, { graph, memoryDir, lexicon, env, cache }) : null;
|
|
10726
|
+
if (ref) {
|
|
10727
|
+
answer = ref.text;
|
|
10728
|
+
via = "reference";
|
|
10729
|
+
recordMiss = false;
|
|
10730
|
+
note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
|
|
10731
|
+
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${ref.article.title}" (revid ${ref.article.revid})`);
|
|
10732
|
+
await appendReferenceIsaFact(memoryDir, ref.key, ref.article, cache);
|
|
10733
|
+
}
|
|
10734
|
+
}
|
|
10308
10735
|
// (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
|
|
10309
10736
|
// wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
|
|
10310
10737
|
// WALL KINDNESS: a second consecutive wall collapses to a one-liner whose
|
|
@@ -10435,7 +10862,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
10435
10862
|
// `goal`: the SAME deduced string the debug trace's own "goal:" line
|
|
10436
10863
|
// carries. Only runAsk ever sets this field, so the always-on goal line is
|
|
10437
10864
|
// scoped to real ask-engine turns by construction.
|
|
10438
|
-
|
|
10865
|
+
// `lane`: the dialogue-act lane — a lane's own override, else the
|
|
10866
|
+
// question-shape lookup — resolved to an ISO act by runTurn's withLast.
|
|
10867
|
+
const lane = dialogueLaneOverride ?? askDialogueLane(envelope?.parsed, query, recordMiss);
|
|
10868
|
+
return { answer, logLines, record, focus: newFocus, detail, effectiveQuery, goal: deduced, lane, ...(planResult ? { plan: planResult } : {}) };
|
|
10439
10869
|
}
|
|
10440
10870
|
|
|
10441
10871
|
/** A non-ask, non-dispatch chat turn (count answer, /stats) — the same
|
|
@@ -10916,21 +11346,63 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
10916
11346
|
const PAGE = 32;
|
|
10917
11347
|
const MORE_RE = /^(?:more|show more|see more|the rest|next|continue|go on)\b[.!?]*$/i;
|
|
10918
11348
|
|
|
10919
|
-
/** "what would break if I change X"
|
|
10920
|
-
*
|
|
10921
|
-
* COUNTERFACTUAL_RE ("if X were deleted, what would break"), which
|
|
10922
|
-
*
|
|
10923
|
-
*
|
|
10924
|
-
*
|
|
10925
|
-
*
|
|
11349
|
+
/** The impact-intent gate — "what would break if I change X" and its natural
|
|
11350
|
+
* neighbours, routed to the same /impact closure. Sibling of normalize.mjs's
|
|
11351
|
+
* COUNTERFACTUAL_RE ("if X were deleted, what would break"), which compiles
|
|
11352
|
+
* to the reverse import closure; these shapes name a CHANGE rather than a
|
|
11353
|
+
* deletion, so they answer with the impact closure /impact itself renders.
|
|
11354
|
+
* The verbs are a closed set on both sides — no general "any verb in a
|
|
11355
|
+
* conditional" fit. The gate runs ahead of the teach classifier and the
|
|
11356
|
+
* relaxation cascade, because an interrogative must never reach the write
|
|
11357
|
+
* boundary ("blast radius of X" was remembered as a fact) and "impact" must
|
|
11358
|
+
* never be fuzzy-read as "import" (the inverse question). */
|
|
11359
|
+
const IMPACT_CHANGE_VERBS = "(?:changed?|modif(?:y|ied)|edits?|edited|touch(?:es|ed)?|updates?|updated|alters?|altered|deletes?|deleted|removes?|removed|drops?|dropped)";
|
|
10926
11360
|
const IMPACT_PARAPHRASE_RE = new RegExp(
|
|
10927
11361
|
"^what\\s+(?:would|will|might|could|does|do)?\\s*"
|
|
10928
|
-
+ "(?:breaks?|fails?|is\\s+affected|are\\s+affected|gets?\\s+affected|be\\s+affected|is\\s+impacted|be\\s+impacted)"
|
|
10929
|
-
+
|
|
10930
|
-
+ "(?:changed?|modif(?:y|ied)|edits?|edited|touch(?:es|ed)?|updates?|updated|alters?|altered)"
|
|
11362
|
+
+ "(?:breaks?|fails?|happens?|stops?\\s+working|is\\s+affected|are\\s+affected|gets?\\s+affected|be\\s+affected|is\\s+impacted|be\\s+impacted)"
|
|
11363
|
+
+ `\\s+if\\s+(?:i|we|you|one|someone)\\s+${IMPACT_CHANGE_VERBS}`
|
|
10931
11364
|
+ "\\s+(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
10932
11365
|
"i",
|
|
10933
11366
|
);
|
|
11367
|
+
// The same counterfactual with the clauses reversed — "if I change X what breaks".
|
|
11368
|
+
const IMPACT_REVERSED_RE = new RegExp(
|
|
11369
|
+
`^if\\s+(?:i|we|you|one|someone)\\s+${IMPACT_CHANGE_VERBS}`
|
|
11370
|
+
+ "\\s+(?:the\\s+)?(.+?),?\\s+what\\s+(?:would\\s+|will\\s+|might\\s+|could\\s+|does\\s+|do\\s+)?"
|
|
11371
|
+
+ "(?:breaks?|fails?|happens?|stops?\\s+working|is\\s+affected|are\\s+affected|gets?\\s+affected|be\\s+affected|would\\s+break|will\\s+break)"
|
|
11372
|
+
+ "[?.!\\s]*$",
|
|
11373
|
+
"i",
|
|
11374
|
+
);
|
|
11375
|
+
// The agentless passive — "what is affected by changing X".
|
|
11376
|
+
const IMPACT_AFFECTED_BY_RE = new RegExp(
|
|
11377
|
+
"^what\\s+(?:is|are|gets?|would\\s+be|will\\s+be)\\s+(?:affected|impacted|broken)\\s+"
|
|
11378
|
+
+ "(?:by|when|if)\\s+(?:i\\s+|we\\s+|you\\s+)?"
|
|
11379
|
+
+ "(?:chang(?:e|es|ing)|edit(?:s|ing)?|modif(?:y|ies|ying)|touch(?:es|ing)?|updat(?:e|es|ing)|delet(?:e|es|ing)|remov(?:e|es|ing)|a\\s+change\\s+to)\\s+"
|
|
11380
|
+
+ "(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
11381
|
+
"i",
|
|
11382
|
+
);
|
|
11383
|
+
// "can I safely delete X" — a change-safety question IS the impact question.
|
|
11384
|
+
const IMPACT_SAFE_CHANGE_RE = new RegExp(
|
|
11385
|
+
"^(?:(?:can|could)\\s+(?:i|we|you|one|someone)\\s+safely|is\\s+it\\s+safe\\s+to)\\s+"
|
|
11386
|
+
+ "(?:change|edit|modify|touch|update|alter|delete|remove|drop)\\s+"
|
|
11387
|
+
+ "(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
11388
|
+
"i",
|
|
11389
|
+
);
|
|
11390
|
+
// The NP form — "blast radius of X", "impact of changing X".
|
|
11391
|
+
const IMPACT_NOUN_RE = new RegExp(
|
|
11392
|
+
"^(?:what(?:'s|\\s+is)\\s+the\\s+)?(?:blast\\s+radius|impact)\\s+(?:of|for)\\s+"
|
|
11393
|
+
+ "(?:chang(?:ing|es)\\s+|editing\\s+|modifying\\s+|touching\\s+|updating\\s+|deleting\\s+|removing\\s+)?"
|
|
11394
|
+
+ "(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
11395
|
+
"i",
|
|
11396
|
+
);
|
|
11397
|
+
const IMPACT_INTENT_RES = [IMPACT_PARAPHRASE_RE, IMPACT_REVERSED_RE, IMPACT_AFFECTED_BY_RE, IMPACT_SAFE_CHANGE_RE, IMPACT_NOUN_RE];
|
|
11398
|
+
/** The impact intent in any of its clause orders -> the subject term, or null. */
|
|
11399
|
+
function matchImpactIntent(line) {
|
|
11400
|
+
for (const re of IMPACT_INTENT_RES) {
|
|
11401
|
+
const m = line.match(re);
|
|
11402
|
+
if (m) return m[1].trim();
|
|
11403
|
+
}
|
|
11404
|
+
return null;
|
|
11405
|
+
}
|
|
10934
11406
|
const joinList = (a) => (a.length > 1 ? `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}` : (a[0] ?? ""));
|
|
10935
11407
|
|
|
10936
11408
|
/** Render the next page of a held remainder (pending: {items:[str], noun}). Returns a
|
|
@@ -10947,6 +11419,251 @@ function morePage(query, { last, focus }) {
|
|
|
10947
11419
|
return turn;
|
|
10948
11420
|
}
|
|
10949
11421
|
|
|
11422
|
+
// ---- the SKOS view: synonym/related-word questions over the store ----
|
|
11423
|
+
// "another word for X" / "synonyms of X" / "what is related to X" read the
|
|
11424
|
+
// store's mgx:synonym / mgx:relatedTo / mgx:similarTo facts through
|
|
11425
|
+
// buildSkosConceptView's minted concepts (relatedForTerm). Routed ahead of
|
|
11426
|
+
// the generic parse, which reads these phrasings as something else entirely.
|
|
11427
|
+
// A term that mints no concept — unknown, or with no synonym/related facts —
|
|
11428
|
+
// misses honestly, naming the term, never a guessed neighbour.
|
|
11429
|
+
const SKOS_SYNONYM_RE = /^(?:another\s+word\s+for|other\s+words\s+for|synonyms?\s+(?:of|for)|what\s+is\s+a\s+synonym\s+(?:of|for))\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
11430
|
+
const SKOS_RELATED_RE = /^(?:what\s+is\s+related\s+to|what\s+relates\s+to|what\s+words\s+are\s+related\s+to)\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
11431
|
+
|
|
11432
|
+
/** The SKOS-view answer for a synonym/related question, or null when the
|
|
11433
|
+
* line is not one. A matched line always answers — a hit lists the group's
|
|
11434
|
+
* other labels and the related concepts; anything else is the honest miss. */
|
|
11435
|
+
async function skosRelatedAnswer(memoryDir, query, cache) {
|
|
11436
|
+
if (!memoryDir) return null;
|
|
11437
|
+
const q = expandContractions(String(query).trim());
|
|
11438
|
+
const syn = q.match(SKOS_SYNONYM_RE);
|
|
11439
|
+
const rel = syn ? null : q.match(SKOS_RELATED_RE);
|
|
11440
|
+
if (!syn && !rel) return null;
|
|
11441
|
+
const term = (syn ?? rel)[1].trim();
|
|
11442
|
+
let hood = null;
|
|
11443
|
+
try { hood = relatedForTerm(await factRows(memoryDir, cache), term); } catch { hood = null; }
|
|
11444
|
+
const parts = [];
|
|
11445
|
+
if (hood?.synonyms?.length) parts.push(`another word for ${term}: ${joinList(hood.synonyms)}`);
|
|
11446
|
+
const relatedLabels = (hood?.related ?? []).map((c) => c.prefLabel);
|
|
11447
|
+
if (relatedLabels.length) parts.push(`related: ${joinList(relatedLabels)}`);
|
|
11448
|
+
if (!parts.length) {
|
|
11449
|
+
return { term, miss: true, text: `I don't know any synonyms or related words for "${term}" yet.` };
|
|
11450
|
+
}
|
|
11451
|
+
return { term, miss: false, text: `${parts.join("; ")} (source: remembered synonym/related facts, read as SKOS)` };
|
|
11452
|
+
}
|
|
11453
|
+
|
|
11454
|
+
// ---- guess-the-number: a closed-loop game over hidden state ----
|
|
11455
|
+
// Two modes on one mechanism. In GUESSER mode the human holds a secret and
|
|
11456
|
+
// tmct searches: a belief interval {lo, hi} narrowed by bisection, one
|
|
11457
|
+
// observation ("higher"/"lower"/"correct") folded in per turn. In THINKER
|
|
11458
|
+
// mode tmct commits a secret up front and each turn is a stateless
|
|
11459
|
+
// comparison against it. The game payload rides the session's plan slot as
|
|
11460
|
+
// a tagged sub-object ({ game: {...} }), so a plan frame and a game never
|
|
11461
|
+
// share the slot — each declines to start while the other is active.
|
|
11462
|
+
|
|
11463
|
+
/** "between A and B" / "up to N" anywhere in an opening line. Loose token
|
|
11464
|
+
* captures (\S+) so a non-numeric bound is SEEN and declined rather than
|
|
11465
|
+
* silently defaulted. */
|
|
11466
|
+
const GAME_BOUNDS_CLAUSE_RE = /\b(?:between\s+(\S+)\s+and\s+(\S+)|up\s+to\s+(\S+))\b/i;
|
|
11467
|
+
const GAME_BOUND_MAX = 1_000_000_000;
|
|
11468
|
+
|
|
11469
|
+
/** The bounds an opening line states — { lo, hi } (default 1–100), or
|
|
11470
|
+
* { problem } naming why the stated range is unplayable. */
|
|
11471
|
+
function parseGameBounds(text) {
|
|
11472
|
+
const m = String(text).match(GAME_BOUNDS_CLAUSE_RE);
|
|
11473
|
+
if (!m) return { lo: 1, hi: 100 };
|
|
11474
|
+
const tokens = (m[3] !== undefined ? ["1", m[3]] : [m[1], m[2]])
|
|
11475
|
+
.map((t) => String(t).replace(/[,.?!]+$/, ""));
|
|
11476
|
+
if (!tokens.every((t) => /^-?\d+$/.test(t))) {
|
|
11477
|
+
return { problem: 'I can only play with whole-number bounds — say "between 1 and 100".' };
|
|
11478
|
+
}
|
|
11479
|
+
const lo = Number(tokens[0]);
|
|
11480
|
+
const hi = Number(tokens[1]);
|
|
11481
|
+
if (Math.abs(lo) > GAME_BOUND_MAX || Math.abs(hi) > GAME_BOUND_MAX) {
|
|
11482
|
+
return { problem: `that range is too big for a fair game — keep both bounds within ${GAME_BOUND_MAX.toLocaleString("en-US")}.` };
|
|
11483
|
+
}
|
|
11484
|
+
if (hi < lo) return { problem: `no number is between ${lo} and ${hi} — that range is empty. Put the smaller bound first.` };
|
|
11485
|
+
if (hi === lo) return { problem: `between ${lo} and ${hi} leaves exactly one number, so there is nothing to guess. Pick a wider range.` };
|
|
11486
|
+
return { lo, hi };
|
|
11487
|
+
}
|
|
11488
|
+
|
|
11489
|
+
// Opening moves, both modes, as closed-set leads + a tail that may only carry
|
|
11490
|
+
// the bounds clause and the closing invitation words — any other tail is a
|
|
11491
|
+
// real sentence and falls through to the ordinary lanes.
|
|
11492
|
+
const GUESSER_OPEN_LEAD_RE = /^(?:i\s*(?:'m|am)\s+thinking\s+of\s+a\s+number|guess\s+my\s+number|guess\s+the\s+number\s+i\s*(?:'m|am)\s+thinking\s+of|guess\s+a\s+number\s+(?:between\s+\S+\s+and\s+\S+\s+|up\s+to\s+\S+\s+)?and\s+i\s*(?:'ll|\s+will)\s+tell\s+you\s+(?:if\s+it\s*(?:'s|\s+is)\s+)?higher\s+or\s+lower)\b(.*)$/i;
|
|
11493
|
+
const THINKER_OPEN_LEAD_RE = /^(?:think\s+of\s+a\s+number)\b(.*)$/i;
|
|
11494
|
+
const GUESSER_OPEN_TAIL_RE = /^[\s,.!?—-]*(?:and\s+)?(?:you\s+)?(?:can\s+|have\s+to\s+|try\s+to\s+)?(?:guess(?:\s+it|\s+what\s+it\s+is)?)?[\s,.!?—-]*$/i;
|
|
11495
|
+
const THINKER_OPEN_TAIL_RE = /^[\s,.!?—-]*(?:and\s+)?(?:i\s*(?:'ll|\s+will)\s+(?:try\s+to\s+)?guess(?:\s+it)?|i\s+guess)?[\s,.!?—-]*$/i;
|
|
11496
|
+
|
|
11497
|
+
/** An opening move — { mode, bounds } — or null. */
|
|
11498
|
+
function matchGameOpening(line) {
|
|
11499
|
+
const l = String(line).trim();
|
|
11500
|
+
const guesser = l.match(GUESSER_OPEN_LEAD_RE);
|
|
11501
|
+
if (guesser && GUESSER_OPEN_TAIL_RE.test(guesser[1].replace(GAME_BOUNDS_CLAUSE_RE, " "))) {
|
|
11502
|
+
return { mode: "guesser", bounds: parseGameBounds(l) };
|
|
11503
|
+
}
|
|
11504
|
+
const thinker = l.match(THINKER_OPEN_LEAD_RE);
|
|
11505
|
+
if (thinker && THINKER_OPEN_TAIL_RE.test(thinker[1].replace(GAME_BOUNDS_CLAUSE_RE, " "))) {
|
|
11506
|
+
return { mode: "thinker", bounds: parseGameBounds(l) };
|
|
11507
|
+
}
|
|
11508
|
+
return null;
|
|
11509
|
+
}
|
|
11510
|
+
|
|
11511
|
+
// Continuation replies, gated STRICTLY on an active game (the same discipline
|
|
11512
|
+
// MORE_RE applies to a held pending remainder): with no game standing none of
|
|
11513
|
+
// these are ever consulted, and mid-game any line that matches none of them
|
|
11514
|
+
// is an ordinary aside — answered by the normal lanes, game untouched.
|
|
11515
|
+
const GAME_STOP_RE = /^(?:ok[,\s]+)?(?:i\s+give\s+up|give\s+up|i\s+quit(?:\s+the\s+game)?|stop\s+(?:the\s+game|playing)|end\s+the\s+game)[.!?\s]*$/i;
|
|
11516
|
+
const GAME_REVEAL_RE = /^(?:just\s+tell\s+me|(?:just\s+)?tell\s+me\s+the\s+(?:number|answer)|what(?:'s|\s+is)\s+(?:the|your)\s+(?:secret\s+)?number|reveal\s+(?:it|the\s+number)|show\s+me\s+the\s+number)[.!?\s]*$/i;
|
|
11517
|
+
const GAME_OBS_HIGHER_RE = /^(?:no[,\s]+)?(?:higher|too\s+low|too\s+small|bigger|greater|go\s+higher|it(?:'s|\s+is)\s+higher)[.!?\s]*$/i;
|
|
11518
|
+
const GAME_OBS_LOWER_RE = /^(?:no[,\s]+)?(?:lower|too\s+high|too\s+big|smaller|less|go\s+lower|it(?:'s|\s+is)\s+lower)[.!?\s]*$/i;
|
|
11519
|
+
const GAME_OBS_CORRECT_RE = /^(?:yes|yep|yeah|correct|you\s+got\s+it|you\s+guessed\s+it|that(?:'s|\s+is)\s+it|that(?:'s|\s+is)\s+right|got\s+it|spot\s+on)[.!?\s]*$/i;
|
|
11520
|
+
const GAME_GUESS_RE = /^(?:is\s+it\s+)?(-?\d{1,12})\s*\??[.!?\s]*$/;
|
|
11521
|
+
const GAME_FALSE_CORRECT_RE = /^(?:but\s+)?you\s+(?:already\s+)?said\s+(?:it\s+was\s+)?(?:correct|right)\b/i;
|
|
11522
|
+
|
|
11523
|
+
/** A natural-language plan frame — the shapes planLaneAnswer owns. Mid-game
|
|
11524
|
+
* these get the one-at-a-time decline instead of clobbering the slot. */
|
|
11525
|
+
function isPlanFrameLine(line) {
|
|
11526
|
+
return GOAL_TEACH_RE.test(line) || GOAL_TEACH_INFINITIVE_RE.test(line)
|
|
11527
|
+
|| GOAL_TEACH_VERBLESS_RE.test(line) || GOAL_TEACH_NP_RE.test(line)
|
|
11528
|
+
|| GOAL_TEACH_IMPERATIVE_RE.test(line) || GOAL_TEACH_CONJUNCTION_RE.test(line)
|
|
11529
|
+
|| PLAN_SOLVE_RE.test(line) || LEGAL_MOVES_RE.test(line);
|
|
11530
|
+
}
|
|
11531
|
+
|
|
11532
|
+
/** The per-turn goal line, table-driven off the live game state. */
|
|
11533
|
+
function gameGoal(game) {
|
|
11534
|
+
if (game.mode === "guesser") return `narrow down your number — currently between ${game.lo} and ${game.hi}`;
|
|
11535
|
+
if (!game.lastHint) return "let you find my secret number I've committed to";
|
|
11536
|
+
return `let you find my secret number — said "${game.lastHint}" so it's ${game.lastHint === "higher" ? "above" : "below"} your last guess`;
|
|
11537
|
+
}
|
|
11538
|
+
|
|
11539
|
+
/** One guesser-mode observation folded into the belief interval, or a
|
|
11540
|
+
* thinker-mode guess compared against the secret. Mutates planHolder.state
|
|
11541
|
+
* (the same slot the plan lane owns) and returns { text, goal?, lane, note },
|
|
11542
|
+
* or null when the line is not a game reply. */
|
|
11543
|
+
function gameContinuationAnswer(line, game, planHolder) {
|
|
11544
|
+
const endGame = () => { planHolder.state = null; };
|
|
11545
|
+
if (game.mode === "guesser") {
|
|
11546
|
+
if (GAME_STOP_RE.test(line)) {
|
|
11547
|
+
endGame();
|
|
11548
|
+
return { text: 'OK, stopping — I never found it. Say "guess my number" any time to play again.', lane: "game-inform", note: "GAME — the game ended on request; the belief interval is discarded" };
|
|
11549
|
+
}
|
|
11550
|
+
if (GAME_OBS_CORRECT_RE.test(line)) {
|
|
11551
|
+
const { guess, guesses } = game;
|
|
11552
|
+
endGame();
|
|
11553
|
+
return { text: `Got it — your number is ${guess}, found in ${guesses} guess${guesses === 1 ? "" : "es"}. Want to play again?`, lane: "game-answer", note: "GAME — the guess was confirmed; game over, won" };
|
|
11554
|
+
}
|
|
11555
|
+
const higher = GAME_OBS_HIGHER_RE.test(line);
|
|
11556
|
+
const lower = !higher && GAME_OBS_LOWER_RE.test(line);
|
|
11557
|
+
if (!higher && !lower) return null;
|
|
11558
|
+
const prior = game.guess;
|
|
11559
|
+
const next = { ...game };
|
|
11560
|
+
if (higher) { next.lo = prior + 1; next.loSetBy = { guess: prior }; }
|
|
11561
|
+
else { next.hi = prior - 1; next.hiSetBy = { guess: prior }; }
|
|
11562
|
+
if (next.lo > next.hi) {
|
|
11563
|
+
// The interval is EMPTY: no number satisfies every observation given,
|
|
11564
|
+
// so name the two observations that cannot both hold and stop guessing
|
|
11565
|
+
// — never a fabricated next guess over a premise known to be false.
|
|
11566
|
+
endGame();
|
|
11567
|
+
const earlier = higher
|
|
11568
|
+
? (game.hiSetBy ? `lower than ${game.hiSetBy.guess}` : `it's between ${game.lo0} and ${game.hi0}`)
|
|
11569
|
+
: (game.loSetBy ? `higher than ${game.loSetBy.guess}` : `it's between ${game.lo0} and ${game.hi0}`);
|
|
11570
|
+
const now = `${higher ? "higher" : "lower"} than ${prior}`;
|
|
11571
|
+
return {
|
|
11572
|
+
text: `That's not possible — you said ${earlier}, and now ${now}, but no number can be both. One of those answers must be wrong. Say "guess my number" to restart.`,
|
|
11573
|
+
lane: "game-answer",
|
|
11574
|
+
note: "GAME — the observations emptied the belief interval; refused to keep guessing under a false premise",
|
|
11575
|
+
};
|
|
11576
|
+
}
|
|
11577
|
+
next.guess = Math.floor((next.lo + next.hi) / 2);
|
|
11578
|
+
next.guesses = game.guesses + 1;
|
|
11579
|
+
planHolder.state = { game: next };
|
|
11580
|
+
return {
|
|
11581
|
+
text: `My guess: ${next.guess}. Say higher, lower, or correct.`,
|
|
11582
|
+
goal: gameGoal(next),
|
|
11583
|
+
lane: "game-inform",
|
|
11584
|
+
note: `GAME — folded "${higher ? "higher" : "lower"}" into the interval and bisected it again`,
|
|
11585
|
+
};
|
|
11586
|
+
}
|
|
11587
|
+
// Thinker mode: tmct holds the ground truth, so every reply is a plain
|
|
11588
|
+
// comparison — and the hint record is authoritative against false claims.
|
|
11589
|
+
if (GAME_STOP_RE.test(line) || GAME_REVEAL_RE.test(line)) {
|
|
11590
|
+
const { secret } = game;
|
|
11591
|
+
endGame();
|
|
11592
|
+
return { text: `The number was ${secret}. Want to play again?`, lane: "game-answer", note: "GAME — revealed the secret on request; game over" };
|
|
11593
|
+
}
|
|
11594
|
+
if (GAME_FALSE_CORRECT_RE.test(line)) {
|
|
11595
|
+
const record = game.lastHint
|
|
11596
|
+
? `my last hint was "${game.lastHint}", after your guess of ${game.lastGuess}`
|
|
11597
|
+
: "you haven't guessed yet";
|
|
11598
|
+
return { text: `I haven't said "correct" yet — ${record}. Keep guessing.`, goal: gameGoal(game), lane: "game-answer", note: "GAME — rebutted a false \"you said correct\" from the game's own hint record" };
|
|
11599
|
+
}
|
|
11600
|
+
const m = String(line).trim().match(GAME_GUESS_RE);
|
|
11601
|
+
if (!m) return null;
|
|
11602
|
+
const guess = Number.parseInt(m[1], 10);
|
|
11603
|
+
if (guess < game.lo0 || guess > game.hi0) {
|
|
11604
|
+
return { text: `${guess} is outside the ${game.lo0} to ${game.hi0} range we agreed — try a number in range.`, goal: gameGoal(game), lane: "game-answer", note: "GAME — an out-of-range guess; declined rather than comparing outside the agreed bounds" };
|
|
11605
|
+
}
|
|
11606
|
+
const next = { ...game, guesses: game.guesses + 1, lastGuess: guess };
|
|
11607
|
+
if (guess === game.secret) {
|
|
11608
|
+
endGame();
|
|
11609
|
+
return { text: `Correct — you got it in ${next.guesses} guess${next.guesses === 1 ? "" : "es"}! The number was ${guess}. Want to play again?`, lane: "game-answer", note: "GAME — the guess matched the secret; game over, won" };
|
|
11610
|
+
}
|
|
11611
|
+
next.lastHint = guess < game.secret ? "higher" : "lower";
|
|
11612
|
+
planHolder.state = { game: next };
|
|
11613
|
+
return { text: `${next.lastHint} — guess again.`, goal: gameGoal(next), lane: "game-answer", note: `GAME — compared the guess against the committed secret: ${next.lastHint}` };
|
|
11614
|
+
}
|
|
11615
|
+
|
|
11616
|
+
/** The whole game lane for one turn: continuations first (active game only),
|
|
11617
|
+
* then opening moves, with the one-at-a-time declines both ways across the
|
|
11618
|
+
* shared plan slot. Null when the turn is not the game's to answer. */
|
|
11619
|
+
function guessNumberTurn(line, { planHolder, env }) {
|
|
11620
|
+
const state = planHolder?.state ?? null;
|
|
11621
|
+
const game = state?.game ?? null;
|
|
11622
|
+
const opening = matchGameOpening(line);
|
|
11623
|
+
if (game) {
|
|
11624
|
+
const continuation = gameContinuationAnswer(line, game, planHolder);
|
|
11625
|
+
if (continuation) return continuation;
|
|
11626
|
+
if (opening) {
|
|
11627
|
+
return { text: `we're already playing — I'm ${game.mode === "guesser" ? "guessing your number" : "holding a secret number"}. Say "I give up" to end this game first.`, lane: "game-inform", note: "GAME — an opening arrived mid-game; declined, the running game stands" };
|
|
11628
|
+
}
|
|
11629
|
+
if (isPlanFrameLine(line)) {
|
|
11630
|
+
return { text: 'a guess-the-number game is active — say "I give up" to end it, then set your goal.', lane: "game-inform", note: "GAME — a plan frame arrived mid-game; the slot holds one thing at a time" };
|
|
11631
|
+
}
|
|
11632
|
+
return null;
|
|
11633
|
+
}
|
|
11634
|
+
if (!opening) return null;
|
|
11635
|
+
const planActive = state && !state.done
|
|
11636
|
+
&& ((Array.isArray(state.goals) && state.goals.length) || (Array.isArray(state.actions) && state.actions.length));
|
|
11637
|
+
if (planActive) {
|
|
11638
|
+
return { text: "a plan is in progress — finish it or start a fresh goal before we play guess-the-number.", lane: "game-inform", note: "GAME — an opening arrived while a plan frame is active; the slot holds one thing at a time" };
|
|
11639
|
+
}
|
|
11640
|
+
if (opening.bounds.problem) {
|
|
11641
|
+
return { text: opening.bounds.problem, lane: "game-inform", note: "GAME — the opening stated an unplayable range; declined honestly" };
|
|
11642
|
+
}
|
|
11643
|
+
const { lo, hi } = opening.bounds;
|
|
11644
|
+
if (opening.mode === "guesser") {
|
|
11645
|
+
const guess = Math.floor((lo + hi) / 2);
|
|
11646
|
+
planHolder.state = { game: { mode: "guesser", lo0: lo, hi0: hi, lo, hi, guess, guesses: 1, loSetBy: null, hiSetBy: null } };
|
|
11647
|
+
return {
|
|
11648
|
+
text: `OK — you're thinking of a number between ${lo} and ${hi}; I'll guess it. My guess: ${guess}. Say higher, lower, or correct.`,
|
|
11649
|
+
goal: `narrow down your number — currently between ${lo} and ${hi}`,
|
|
11650
|
+
lane: "game-inform",
|
|
11651
|
+
note: "GAME — guesser mode opened; the belief interval starts at the agreed bounds and the first guess is its midpoint",
|
|
11652
|
+
};
|
|
11653
|
+
}
|
|
11654
|
+
const envSecret = Number.parseInt(String(env?.TMCT_GAME_SECRET ?? ""), 10);
|
|
11655
|
+
const secret = Number.isSafeInteger(envSecret) && envSecret >= lo && envSecret <= hi
|
|
11656
|
+
? envSecret
|
|
11657
|
+
: lo + Math.floor(Math.random() * (hi - lo + 1));
|
|
11658
|
+
planHolder.state = { game: { mode: "thinker", lo0: lo, hi0: hi, secret, guesses: 0, lastHint: null, lastGuess: null } };
|
|
11659
|
+
return {
|
|
11660
|
+
text: `Done — I've thought of a number between ${lo} and ${hi}. Guess it, and I'll say higher, lower, or correct.`,
|
|
11661
|
+
goal: "let you find my secret number I've committed to",
|
|
11662
|
+
lane: "game-inform",
|
|
11663
|
+
note: "GAME — thinker mode opened; the secret is committed for the whole game",
|
|
11664
|
+
};
|
|
11665
|
+
}
|
|
11666
|
+
|
|
10950
11667
|
// "I want you to search for Widget" / "I'd like you to search for Widget" —
|
|
10951
11668
|
// a closed-set indirect-request wrapper, checked VERY early. Without this it
|
|
10952
11669
|
// is mis-swallowed by GENERAL_VERB_TEACH_RE as a bare teach triple (subject
|
|
@@ -10956,6 +11673,40 @@ function morePage(query, { last, focus }) {
|
|
|
10956
11673
|
// throughout this file, and centralizing it risks double-processing.
|
|
10957
11674
|
const INDIRECT_REQUEST_RE = /^(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+would)\s+like\s+you\s+to\s+)\s*(.+)$/i;
|
|
10958
11675
|
|
|
11676
|
+
// First-person desire openers for a vocabulary question — "i wanna know about
|
|
11677
|
+
// a horse", "you tell me about dog", "let me know about a dog" — and the
|
|
11678
|
+
// known-kinds enumeration ("what animals do you know", "list the animals you
|
|
11679
|
+
// know"). Each rewrites to the canonical question its lane already answers
|
|
11680
|
+
// ("tell me about X" / "what is a X"), BEFORE any dispatch lane sees the
|
|
11681
|
+
// text: the leading "i" otherwise reads as a teach subject, so a read-only
|
|
11682
|
+
// question asserted an intent it doesn't have. Closed set, rewrite-only —
|
|
11683
|
+
// same discipline as INDIRECT_REQUEST_RE above.
|
|
11684
|
+
const DESIRE_ABOUT_RE = /^i\s+(?:wanna|want\s+to|wanted\s+to|(?:'d\s+|would\s+)?like\s+to|need\s+to)\s+(?:know|learn|hear)\s+(?:(?:more|something)\s+)?about\s+(.+?)[?.!\s]*$/i;
|
|
11685
|
+
const TELL_ABOUT_VARIANT_RE = /^(?:you\s+tell\s+me|let\s+me\s+know|fill\s+me\s+in)\s+(?:(?:more|something)\s+)?about\s+(.+?)[?.!\s]*$/i;
|
|
11686
|
+
// "facts"/"things"/"stuff" ask for the whole-store recall, not a kind's
|
|
11687
|
+
// members — those keep their own lane.
|
|
11688
|
+
const KNOWN_KINDS_RE = /^(?:(?:so|uh|um|well|ok|okay),?\s+)*(?:what|which)\s+(?!else\b|all\b|facts?\b|things?\b|stuff\b)([a-z][\w-]*)\s+do\s+(?:you|u)\s+know(?:\s+(?:about|of|so\s+far))?[?.!\s]*$/i;
|
|
11689
|
+
const LIST_KNOWN_KINDS_RE = /^list\s+(?:the\s+|all\s+(?:the\s+)?)?(?!facts?\b|things?\b|stuff\b)([a-z][\w-]*)\s+(?:that\s+)?(?:you|u)\s+know(?:\s+(?:about|of))?[?.!\s]*$/i;
|
|
11690
|
+
// "if something is a dog then it is a pet" — the universal conditional IS the
|
|
11691
|
+
// universal subclass teach in a conditional coat, so it rewrites to the
|
|
11692
|
+
// "every X is a Y" surface the teach path already stores (with its quantifier
|
|
11693
|
+
// and its own confirmation). Closed to the indefinite-pronoun subject: a
|
|
11694
|
+
// conditional over a NAMED subject or an arbitrary property is a rule, not a
|
|
11695
|
+
// subclass fact, and stays outside this frame.
|
|
11696
|
+
const UNIVERSAL_CONDITIONAL_RE = /^if\s+(?:something|somebody|someone|anything)\s+is\s+an?\s+([\w-]+)\s*,?\s*(?:then\s+)?(?:it|they)\s+(?:is|are)\s+an?\s+([\w-]+)[.!?\s]*$/i;
|
|
11697
|
+
function rewriteVocabOpener(line) {
|
|
11698
|
+
let m = line.match(DESIRE_ABOUT_RE) || line.match(TELL_ABOUT_VARIANT_RE);
|
|
11699
|
+
if (m) return `tell me about ${m[1].trim()}`;
|
|
11700
|
+
m = line.match(UNIVERSAL_CONDITIONAL_RE);
|
|
11701
|
+
if (m) return `every ${m[1]} is ${indefiniteArticleFor(m[2])} ${m[2]}`;
|
|
11702
|
+
m = line.match(KNOWN_KINDS_RE) || line.match(LIST_KNOWN_KINDS_RE);
|
|
11703
|
+
if (m) {
|
|
11704
|
+
const noun = teachableSubjectOf(m[1]);
|
|
11705
|
+
return `what is ${indefiniteArticleFor(noun)} ${noun}`;
|
|
11706
|
+
}
|
|
11707
|
+
return null;
|
|
11708
|
+
}
|
|
11709
|
+
|
|
10959
11710
|
/** A DISCONTIGUOUS verb frame, "SUBJECT uses OBJECT as its/a base(class)" —
|
|
10960
11711
|
* "uses" is split from its own qualifier ("as its base") around the object,
|
|
10961
11712
|
* so no contiguous phrase-table entry could ever register it, and "uses"
|
|
@@ -11080,7 +11831,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11080
11831
|
// the ORIGINAL `line` survives untouched for record.query/logLines fidelity
|
|
11081
11832
|
// — restored centrally inside withLast (below), once, for every dispatch path.
|
|
11082
11833
|
const indirectMatch = line.match(INDIRECT_REQUEST_RE);
|
|
11083
|
-
const
|
|
11834
|
+
const indirectLine = indirectMatch ? indirectMatch[1].trim() : line;
|
|
11835
|
+
const preRewriteLine = rewriteVocabOpener(indirectLine) || indirectLine;
|
|
11084
11836
|
// rewriteUsesAsBaseFrame's discontiguous-frame rewrite: applied here, once,
|
|
11085
11837
|
// before ANY dispatch lane sees the text. Null (no-op) for every turn that
|
|
11086
11838
|
// doesn't match one of the four discontiguous shapes.
|
|
@@ -11126,7 +11878,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11126
11878
|
// what the shell prints. The narrate block is applied AFTER `last` is
|
|
11127
11879
|
// captured from the PRE-narration finished result.
|
|
11128
11880
|
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
11129
|
-
const finished = finish(result, { graph });
|
|
11881
|
+
const finished = attachDialogueAct(finish(result, { graph }), trace);
|
|
11130
11882
|
// Every dispatch path below built its own record off `workingLine` (the
|
|
11131
11883
|
// indirect-request wrapper stripped and/or the discontiguous-frame
|
|
11132
11884
|
// rewrite applied) — restore the ORIGINAL raw `line` into record.query
|
|
@@ -11166,6 +11918,26 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11166
11918
|
const bareCmd = asBareCommand(workingLine);
|
|
11167
11919
|
if (bareCmd) return withLast(await runCommand(bareCmd, ctx), "use a specific tool/command directly");
|
|
11168
11920
|
|
|
11921
|
+
// GUESS-THE-NUMBER — opening moves, and (with a game standing) the
|
|
11922
|
+
// closed-set continuation replies. Checked before the conversational layer
|
|
11923
|
+
// because the guesser-mode observations ("yes", "got it") share words with
|
|
11924
|
+
// the acknowledgement sets, and before assertTurn/runAsk because an opening
|
|
11925
|
+
// line would otherwise read as a declarative to remember. A mid-game line
|
|
11926
|
+
// matching no game shape returns null here and the game stands untouched.
|
|
11927
|
+
{
|
|
11928
|
+
const gameTurn = guessNumberTurn(workingLine, { planHolder, env });
|
|
11929
|
+
if (gameTurn) {
|
|
11930
|
+
note(trace, `lane: ${gameTurn.note}`);
|
|
11931
|
+
if (gameTurn.goal) note(trace, `goal: ${gameTurn.goal}`);
|
|
11932
|
+
const result = plainTurn(workingLine, gameTurn.text, { via: "game", focus });
|
|
11933
|
+
if (gameTurn.goal) result.goal = gameTurn.goal;
|
|
11934
|
+
result.lane = gameTurn.lane;
|
|
11935
|
+
const rec = withLast(result, gameTurn.goal ?? "play the guessing game");
|
|
11936
|
+
rec.planState = planHolder.state;
|
|
11937
|
+
return rec;
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
|
|
11169
11941
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
11170
11942
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
11171
11943
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|
|
@@ -11183,7 +11955,9 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11183
11955
|
const step = await executePlanStep(planHolder, { memoryDir, sessionId });
|
|
11184
11956
|
note(trace, `goal: ${step.deduced}`);
|
|
11185
11957
|
note(trace, "lane: PLAN NEXT — executed the active plan's next move as an @stepK snapshot write");
|
|
11186
|
-
const
|
|
11958
|
+
const stepTurn = plainTurn(workingLine, step.text, { via: "plan", focus });
|
|
11959
|
+
stepTurn.lane = "imperative";
|
|
11960
|
+
const rec = withLast(stepTurn, step.deduced);
|
|
11187
11961
|
rec.planState = planHolder.state;
|
|
11188
11962
|
return rec;
|
|
11189
11963
|
}
|
|
@@ -11214,20 +11988,21 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11214
11988
|
return withLast(morePage(workingLine, ctx), "continue viewing a previous long listing");
|
|
11215
11989
|
}
|
|
11216
11990
|
|
|
11217
|
-
// "what would break if I change X" / "
|
|
11991
|
+
// "what would break if I change X" / "if I change X what breaks" / "blast
|
|
11992
|
+
// radius of X" / "impact of X" / "can I safely delete X" — the impact
|
|
11218
11993
|
// closure, in the words people actually ask for it in. With no frame of its
|
|
11219
|
-
// own
|
|
11220
|
-
//
|
|
11221
|
-
//
|
|
11222
|
-
//
|
|
11223
|
-
// its wording ("Impact of changing
|
|
11224
|
-
// hypothetical.
|
|
11225
|
-
const
|
|
11226
|
-
if (
|
|
11994
|
+
// own each of these reached a wrong lane: the forward form's residue
|
|
11995
|
+
// ("break I") read as a subject for the history lane's `touches`, the NP
|
|
11996
|
+
// form fell to the teach lane (a read-only question mutating memory) or to
|
|
11997
|
+
// the fuzzy corrector ("impact" read as "import", the inverse question).
|
|
11998
|
+
// /impact's own closure is the answer, and its wording ("Impact of changing
|
|
11999
|
+
// X") already says the change is hypothetical.
|
|
12000
|
+
const impactSubject = matchImpactIntent(workingLine);
|
|
12001
|
+
if (impactSubject) {
|
|
11227
12002
|
const impactDeduced = "understand what a change to this module would reach (impact closure)";
|
|
11228
12003
|
note(trace, `goal: ${impactDeduced}`);
|
|
11229
|
-
note(trace, `lane:
|
|
11230
|
-
return withLast(await runCommand(`/impact ${
|
|
12004
|
+
note(trace, `lane: impact intent matched -> /impact ${impactSubject}`);
|
|
12005
|
+
return withLast(await runCommand(`/impact ${impactSubject}`, ctx), impactDeduced);
|
|
11231
12006
|
}
|
|
11232
12007
|
|
|
11233
12008
|
// Multi-sentence pre-split — one message carrying several sentences
|
|
@@ -11293,6 +12068,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11293
12068
|
if (asserted) {
|
|
11294
12069
|
note(trace, "goal: teach/remember a new fact (declarative ACE sentence)");
|
|
11295
12070
|
note(trace, "lane: assertTurn — grammar/ace.mjs parseAce matched a full triple with no residue");
|
|
12071
|
+
asserted.lane = "teach";
|
|
11296
12072
|
return withLast(asserted, "teach/remember a new fact");
|
|
11297
12073
|
}
|
|
11298
12074
|
// Bare declarative taxonomy (hyphenated-instance membership, article-led
|
|
@@ -11302,7 +12078,26 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11302
12078
|
if (taxonomy) {
|
|
11303
12079
|
note(trace, "goal: teach/remember a new fact (bare declarative taxonomy)");
|
|
11304
12080
|
note(trace, "lane: bareTaxonomyTeach — hyphenated-instance or article-led kind-of declarative, stored before the ask engine could parse it as a question");
|
|
11305
|
-
|
|
12081
|
+
const taxonomyTurn = plainTurn(workingLine, taxonomy.text, { via: taxonomy.via, miss: taxonomy.miss, focus });
|
|
12082
|
+
if (!taxonomy.miss) taxonomyTurn.lane = "teach";
|
|
12083
|
+
return withLast(taxonomyTurn, "teach/remember a new fact");
|
|
12084
|
+
}
|
|
12085
|
+
}
|
|
12086
|
+
// Synonym/related-word questions read the store through the SKOS view.
|
|
12087
|
+
// Routed before the ask engine: the generic parse reads "another word for
|
|
12088
|
+
// X" as a bare object search and "what is related to X" through
|
|
12089
|
+
// BARE_WHATIS_RE, both wrong lanes for this question.
|
|
12090
|
+
if (memoryDir) {
|
|
12091
|
+
const skos = await skosRelatedAnswer(memoryDir, workingLine, factRowsCache);
|
|
12092
|
+
if (skos) {
|
|
12093
|
+
const goal = `surface the remembered synonym/related-word neighbourhood of "${skos.term}"`;
|
|
12094
|
+
note(trace, `goal: ${goal}`);
|
|
12095
|
+
note(trace, `lane: SKOS VIEW — a synonym/related question ${skos.miss ? "matched but the store holds no such facts (honest miss)" : "answered from the store's relation facts"}`);
|
|
12096
|
+
if (!skos.miss) note(trace, "source: .tmct/memory Facts (mgx:synonym/mgx:relatedTo/mgx:similarTo, read as skos:altLabel/skos:related)");
|
|
12097
|
+
const skosTurn = plainTurn(workingLine, skos.text, { via: skos.miss ? "miss" : "fact", miss: skos.miss, focus });
|
|
12098
|
+
if (!skos.miss) skosTurn.goal = goal;
|
|
12099
|
+
skosTurn.lane = skos.miss ? "honest-miss" : "ask-set";
|
|
12100
|
+
return withLast(skosTurn, goal);
|
|
11306
12101
|
}
|
|
11307
12102
|
}
|
|
11308
12103
|
// MEMORY-STORE counts first ("how many facts / utterances do you know") — the
|