@polycode-projects/the-mechanical-code-talker 2.5.0 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/bin/tmct.mjs +8 -5
- package/corpus/LICENSES.json +7 -0
- package/corpus/README.md +3 -3
- 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 +8 -4
- package/src/adapters/corpus/reference-pack.mjs +107 -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 +155 -0
- package/src/domain/grammar/lexicon-core.json +1 -1
- 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 +499 -123
- package/src/services/init.mjs +2 -2
- package/src/surfaces/web/chat-browser-entry.mjs +87 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +865 -74
- 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/ROADMAP.md +0 -130
package/src/services/chat.mjs
CHANGED
|
@@ -820,7 +820,17 @@ export function renderStats(graph) {
|
|
|
820
820
|
* which meant "who are you" always got the "here's what I can query" blurb and
|
|
821
821
|
* never a self-description — split so each gets the answer it actually asked for. */
|
|
822
822
|
const CAPABILITY_PHRASES = [
|
|
823
|
-
|
|
823
|
+
// The "so"/"uh"/"well" lead and the "for me"/"then" tail are pure discourse
|
|
824
|
+
// filler on the same question — tolerated so the casual forms land on the
|
|
825
|
+
// same orientation answer instead of the parse wall.
|
|
826
|
+
/^(?:(?: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,
|
|
827
|
+
/^(?:(?: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,
|
|
828
|
+
// "what have you got" / "what do you have" — the overview question in its
|
|
829
|
+
// casual spelling; without a frame, "got" parsed as a defines object.
|
|
830
|
+
/^what (?:have|do) (?:you|u) (?:got|have)(?:\s+for\s+me)?(?:\s+(?:here|then|today))?\??$/i,
|
|
831
|
+
// "tell me about this repo" — the orientation request by name; the
|
|
832
|
+
// vocabulary touch lane must not read "this repo" as a concept term.
|
|
833
|
+
/^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
834
|
// "what can you actually help with" — the natural pivot from small talk
|
|
825
835
|
// into a capability question; not covered by the "do" pair above since
|
|
826
836
|
// neither accepts "help (me)? with" as a synonym tail for "do".
|
|
@@ -2141,7 +2151,17 @@ const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
|
2141
2151
|
// class term, the preposition and the target — the caller singularizes the
|
|
2142
2152
|
// term and normalizes "onto"→"on" before the same verbless resolution runs.
|
|
2143
2153
|
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");
|
|
2154
|
+
`^(?:get|put|place|stack)\\s+(?:(every|each|all|both)\\s+)?(?:the\\s+)?([\\w-]+?)\\s+(${PREP_SRC})\\s+([\\w-]+)[?.!]*$`, "i");
|
|
2155
|
+
// The bare-NP voicing of the same goal ("the goal is all disks on peg-c") —
|
|
2156
|
+
// no "that", no "for", no verb. Folds into the verbless resolution exactly
|
|
2157
|
+
// like the imperative above (same captures, same singularize/prep fold).
|
|
2158
|
+
const GOAL_TEACH_NP_RE = new RegExp(
|
|
2159
|
+
`^the\\s+goal\\s+is\\s+(?:(every|each|all|both)\\s+)?(?:the\\s+)?([\\w-]+?)\\s+(${PREP_SRC})\\s+([\\w-]+)[?.!]*$`, "i");
|
|
2160
|
+
// A conjunction of goal atoms ("the goal is that disk-1 rests on peg-b and
|
|
2161
|
+
// disk-3 rests on peg-c") — each conjunct compiles to its own goal spec.
|
|
2162
|
+
const GOAL_TEACH_CONJUNCTION_RE = /^the\s+goal\s+is\s+that\s+(.+?)[.!?]*$/i;
|
|
2163
|
+
const GOAL_CONJUNCT_RE = new RegExp(
|
|
2164
|
+
`^(?:(every|each|all)\\s+)?([\\w-]+)\\s+([a-z]+s)\\s+(${PREP_SRC})\\s+([\\w-]+)$`, "i");
|
|
2145
2165
|
// Plan follow-up questions, answered off the ACTIVE plan state (never invented
|
|
2146
2166
|
// when no plan stands). "next move"/"continue" EXECUTE (PLAN_NEXT_RE above); these
|
|
2147
2167
|
// three only REPORT.
|
|
@@ -2158,6 +2178,11 @@ const BOARD_REVERSE_LOC_RE = new RegExp(
|
|
|
2158
2178
|
const BOARD_FORWARD_LOC_RE = new RegExp(
|
|
2159
2179
|
`^what\\s+(?:does|do|is)\\s+([\\w-]+)\\s+([a-z]+)(?:\\s+(${PREP_SRC}))?[?.!\\s]*$`, "i");
|
|
2160
2180
|
const BOARD_WHERE_RE = /^(?:where\s+is|where's)\s+([\w-]+)(?:\s+now)?[?.!\s]*$/i;
|
|
2181
|
+
// "where does disk-1 rest?" — the verbed spelling of the same board read;
|
|
2182
|
+
// without it the phrasing fell through to the code definition-locator.
|
|
2183
|
+
const BOARD_WHERE_DOES_RE = /^where\s+does\s+([\w-]+)\s+([a-z]+)(?:\s+now)?[?.!\s]*$/i;
|
|
2184
|
+
// "where is every disk" — the same read over every member of a taught class.
|
|
2185
|
+
const BOARD_WHERE_EVERY_RE = /^where\s+(?:is|are)\s+(?:every|each|all(?:\s+the)?)\s+([\w-]+?)[?.!\s]*$/i;
|
|
2161
2186
|
|
|
2162
2187
|
/** "<X> is <adjective>" — the property teach payload (wrapper-REQUIRED): a lazy
|
|
2163
2188
|
* subject and a single bare complement word. Never matches the "is a <noun>"
|
|
@@ -2693,6 +2718,11 @@ const GENERAL_VERB_DETERMINER_TEACH_RE = new RegExp(
|
|
|
2693
2718
|
`^(?:the\\s+|an?\\s+)([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+([a-z]+)\\s+(${PREP_SRC})\\s+(.+?)[.!?]*$`,
|
|
2694
2719
|
"i",
|
|
2695
2720
|
);
|
|
2721
|
+
/** The quantified possession teach ("every dog has fur", "all dogs have
|
|
2722
|
+
* tails") — the closed has/have verb pins the split the way the preposition
|
|
2723
|
+
* pins GENERAL_VERB_DETERMINER_TEACH_RE's, so a universal quantifier can
|
|
2724
|
+
* lead without any verb-position guessing. */
|
|
2725
|
+
const QUANTIFIED_HAS_TEACH_RE = /^(?:every|each|all)\s+([\w'-]+)\s+(?:has|have)\s+(.+?)[.!?]*$/i;
|
|
2696
2726
|
/** Verbs owned by an earlier, more specific recognizer in this lane — is/are
|
|
2697
2727
|
* (class-membership/property, above) and owns/maintains (ownership, above).
|
|
2698
2728
|
* generalVerbTeach declines outright on these so it can never race a more
|
|
@@ -2837,13 +2867,20 @@ async function generalVerbTeach(payload) {
|
|
|
2837
2867
|
// with the preposition pinning the verb; a sentence that frame can't pin
|
|
2838
2868
|
// declines here exactly as it always has.
|
|
2839
2869
|
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) {
|
|
2840
|
-
const
|
|
2841
|
-
if (
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2870
|
+
const quantHas = p.match(QUANTIFIED_HAS_TEACH_RE);
|
|
2871
|
+
if (quantHas) {
|
|
2872
|
+
subjectRaw = singularizeSurface(quantHas[1]);
|
|
2873
|
+
verbRaw = "has";
|
|
2874
|
+
objectRaw = quantHas[2];
|
|
2875
|
+
} else {
|
|
2876
|
+
const det = p.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
2877
|
+
if (!det) return null; // not a bare-name subject, and no preposition to pin the verb
|
|
2878
|
+
subjectRaw = det[1];
|
|
2879
|
+
verbRaw = det[2];
|
|
2880
|
+
// hand the preposition back to the shared fold below, so the minted
|
|
2881
|
+
// predicate comes from the one place that mints it
|
|
2882
|
+
objectRaw = `${det[3]} ${det[4]}`;
|
|
2883
|
+
}
|
|
2847
2884
|
}
|
|
2848
2885
|
const verb = verbRaw.toLowerCase();
|
|
2849
2886
|
if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
|
|
@@ -3462,7 +3499,22 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3462
3499
|
};
|
|
3463
3500
|
}
|
|
3464
3501
|
}
|
|
3465
|
-
//
|
|
3502
|
+
// Nothing stored to disagree with. The gate above is right to refuse
|
|
3503
|
+
// storing a bare negative with no positive behind it — but a subject
|
|
3504
|
+
// the store has never heard of fell PAST every teach lane onto the
|
|
3505
|
+
// code-question bootstrap message, which reads as a different product.
|
|
3506
|
+
// Decline by name instead, saying what would make the claim usable. A
|
|
3507
|
+
// KNOWN subject still falls through — the property/relation frames
|
|
3508
|
+
// downstream own those sentences.
|
|
3509
|
+
if (!priorRows.some((r) => r.subject === negSubject || r.object === negSubject)) {
|
|
3510
|
+
return {
|
|
3511
|
+
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. `
|
|
3512
|
+
+ `Teach me "${negSubject} is ${indefiniteArticleFor(negObject)} ${negObject}" first if that's the disagreement you mean, `
|
|
3513
|
+
+ `or "no ${negSubject} is a ${negObject}" to store the exclusion outright.`,
|
|
3514
|
+
via: "teach-miss", miss: true,
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
// a known subject with no stored positive — fall through (see the gate above).
|
|
3466
3518
|
}
|
|
3467
3519
|
|
|
3468
3520
|
// RETRACTION — "forget that X is a Y": wires the data-layer retraction
|
|
@@ -3480,9 +3532,9 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3480
3532
|
// claiming a specific, possibly-wrong reason.
|
|
3481
3533
|
if (retractForgetMatch) {
|
|
3482
3534
|
const { retractSubClassOf } = await import("../domain/syllogise.mjs");
|
|
3483
|
-
const { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts } = await import("../adapters/memory/core.mjs");
|
|
3535
|
+
const { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts, appendFacts: appendFactsForRetract } = await import("../adapters/memory/core.mjs");
|
|
3484
3536
|
const result = await retractSubClassOf(memoryDir, retractSubject, retractObject, {
|
|
3485
|
-
store: { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts },
|
|
3537
|
+
store: { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts, appendFacts: appendFactsForRetract },
|
|
3486
3538
|
});
|
|
3487
3539
|
if (result.found) {
|
|
3488
3540
|
const extra = result.count - 1; // beyond the target fact itself
|
|
@@ -3947,7 +3999,9 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3947
3999
|
// of THAT subject — the same word generalVerbTeach will store — and leave
|
|
3948
4000
|
// every other sentence reading its first word exactly as before.
|
|
3949
4001
|
const detLed = raw.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
3950
|
-
const
|
|
4002
|
+
const quantHasLed = detLed ? null : raw.match(QUANTIFIED_HAS_TEACH_RE);
|
|
4003
|
+
const subjectWord = detLed ? detLed[1].split(/\s+/).pop()
|
|
4004
|
+
: (quantHasLed ? singularizeSurface(quantHasLed[1]) : raw.match(/^([\w'-]+)/)?.[1]);
|
|
3951
4005
|
if (subjectWord && (await subjectIsNounOrPropn(subjectWord))) {
|
|
3952
4006
|
// A PLURAL explicit-capability surface ("wrens can hum") whose
|
|
3953
4007
|
// SINGULAR is a grounded term stores under the singular first — the
|
|
@@ -4285,15 +4339,48 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
4285
4339
|
// rich, module-grain overview with a thin one. Gated on the term looking
|
|
4286
4340
|
// like a path, so this widens the lane by exactly the shape that was
|
|
4287
4341
|
// 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
|
-
|
|
4342
|
+
const identityMatch = m ? null : (q.match(MODULE_IDENTITY_RE)?.[1]?.trim() ?? null);
|
|
4343
|
+
const phrase = m ? m[1].trim() : (identityMatch ?? q);
|
|
4344
|
+
if (!phrase) return null;
|
|
4345
|
+
if (/^(?:it|this|that|they|them)$/i.test(phrase)) return null;
|
|
4346
|
+
const bare = phrase.replace(/^(?:the|a|an)\s+/i, "").trim();
|
|
4347
|
+
const phraseWords = bare.split(/\s+/);
|
|
4348
|
+
const pathTail = phraseWords[phraseWords.length - 1];
|
|
4349
|
+
// Only an ANCHORED phrasing (the orient/purpose match, or the "what is X"
|
|
4350
|
+
// identity match) may read a modifier-plus-path-tail phrase — the bare-q
|
|
4351
|
+
// fallback stays gated to a lone path shape, or any sentence that happens
|
|
4352
|
+
// to end in a module path would be claimed here.
|
|
4353
|
+
const tailLooksLikePath = !!(m || identityMatch) && phraseWords.length > 1 && MODULE_PATH_RE.test(pathTail);
|
|
4354
|
+
// The identity phrasing ("what is <term>") only ever claims a path-shaped
|
|
4355
|
+
// term — bare, or with modifier words ahead of a path-shaped tail; the
|
|
4356
|
+
// orient/purpose phrasings carry their own anchors.
|
|
4357
|
+
if (!m && !MODULE_PATH_RE.test(bare) && !tailLooksLikePath) return null;
|
|
4358
|
+
const ent = await resolveEntity(graph, m ? phrase : bare);
|
|
4359
|
+
if (ent) {
|
|
4360
|
+
const ind = graph.byId?.get?.(ent.id);
|
|
4361
|
+
if (!ind) return null;
|
|
4362
|
+
return { text: moduleOverviewText(graph, ind), via: "meta" };
|
|
4363
|
+
}
|
|
4364
|
+
// The stale-modifier residue guard the ask engine's resolver applies,
|
|
4365
|
+
// carried into this lane: modifier words the graph has no reading for never
|
|
4366
|
+
// resolve past silently ("the OLD store.mjs" is not store.mjs — the
|
|
4367
|
+
// modifier may be the question). Decline by name, pointing at the near
|
|
4368
|
+
// match, instead of falling to the bare wall.
|
|
4369
|
+
if (tailLooksLikePath) {
|
|
4370
|
+
const tailEnt = await resolveEntity(graph, pathTail);
|
|
4371
|
+
if (tailEnt) {
|
|
4372
|
+
const residue = phraseWords.slice(0, -1);
|
|
4373
|
+
const quoted = residue.map((w) => `"${w}"`).join(" and ");
|
|
4374
|
+
const names = residue.length === 1 ? "names" : "name";
|
|
4375
|
+
const past = residue.length === 1 ? "it" : "them";
|
|
4376
|
+
return {
|
|
4377
|
+
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}?`,
|
|
4378
|
+
via: "meta",
|
|
4379
|
+
miss: true,
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
return null;
|
|
4297
4384
|
}
|
|
4298
4385
|
|
|
4299
4386
|
async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null, focus = null }) {
|
|
@@ -5042,6 +5129,21 @@ function isaPolarityReply(hit, negHit) {
|
|
|
5042
5129
|
return null;
|
|
5043
5130
|
}
|
|
5044
5131
|
|
|
5132
|
+
/** The verdict when a would-be "yes" (a stored fact or a proof chain) crosses
|
|
5133
|
+
* a stored disjointness on the same resolved chain: name both stored facts
|
|
5134
|
+
* and refuse to conclude. A proof is the strongest honesty claim this file
|
|
5135
|
+
* makes, and certifying one side of a stored contradiction would launder the
|
|
5136
|
+
* inconsistency as a derivation — so neither side wins, same discipline as
|
|
5137
|
+
* isaPolarityReply's both-sides verdict. */
|
|
5138
|
+
function isaInconsistencyRefusal(posFact, disjointFact) {
|
|
5139
|
+
const cite = (f) => `${factPhrase(f)}${f.provenance ? ` (source: ${f.provenance})` : ""}`;
|
|
5140
|
+
return {
|
|
5141
|
+
text: `you've told me both ${cite(posFact)} and ${cite(disjointFact)} — together those contradict, and I won't derive an answer from an inconsistency. `
|
|
5142
|
+
+ `To settle it, say "forget that ${posFact.subject} is ${indefiniteArticleFor(posFact.object)} ${posFact.object}".`,
|
|
5143
|
+
replace: true,
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
|
|
5045
5147
|
/** PROOF-CHAIN RECEIPT — "renderable as a chain of thought in words": render
|
|
5046
5148
|
* an ordered list of
|
|
5047
5149
|
* premise Fact rows as one continuous argument — "cache is a kind of store;
|
|
@@ -5142,6 +5244,17 @@ function teachableSubjectOf(subject) {
|
|
|
5142
5244
|
}
|
|
5143
5245
|
}
|
|
5144
5246
|
|
|
5247
|
+
/** The teach-shaped restatement of a QUANTIFIED-PLURAL subject: "all dogs"
|
|
5248
|
+
* folds to "a dog", so an offered sentence stays grammatical and teachable —
|
|
5249
|
+
* echoing the quantifier into a singular frame produced "all dogs is
|
|
5250
|
+
* mortal". Any other subject keeps teachableSubjectOf's own reading. */
|
|
5251
|
+
function suggestibleSubjectPhrase(subject) {
|
|
5252
|
+
const m = String(subject || "").trim().match(/^(?:all|every|each|both|most|some)\s+([\w-]+)$/i);
|
|
5253
|
+
if (!m) return teachableSubjectOf(subject);
|
|
5254
|
+
const singular = teachableSubjectOf(singularizeSurface(m[1]));
|
|
5255
|
+
return `${indefiniteArticleFor(singular)} ${singular}`;
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5145
5258
|
/** A leading universal quantifier, which is scaffolding rather than part of a
|
|
5146
5259
|
* name. The teach frames strip exactly these before storing (UNKNOWN_SUBJECT_RE
|
|
5147
5260
|
* above carries the same set), so no fact is ever stored under a subject that
|
|
@@ -6149,8 +6262,19 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6149
6262
|
if (isa) {
|
|
6150
6263
|
const subj = factTermVariants(normFactTerm, isa[1]);
|
|
6151
6264
|
const obj = factTermVariants(normFactTerm, isa[2]);
|
|
6152
|
-
const isaRows = await
|
|
6265
|
+
const isaRows = await factRows(memoryDir, cache);
|
|
6153
6266
|
const onTerms = (f) => subj.has(f.subject) && obj.has(f.object);
|
|
6267
|
+
// A TAUGHT disjointness touching either asked term can flip or veto the
|
|
6268
|
+
// verdict — it is the negative side of the polarity when it links the
|
|
6269
|
+
// asked terms directly, and a positive whose ⊑-chain crosses one is a
|
|
6270
|
+
// stored contradiction, not a yes. That reasoning (and its refusal) lives
|
|
6271
|
+
// in the full is-a ladder, so this quick reader stands aside for it
|
|
6272
|
+
// rather than answering a yes it hasn't checked.
|
|
6273
|
+
const { DISJOINT_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
6274
|
+
const touchesAskedTerm = (f) => subj.has(f.subject) || subj.has(f.object) || obj.has(f.subject) || obj.has(f.object);
|
|
6275
|
+
if (isaRows.some((f) => f.predicate === DISJOINT_PREDICATE && isOperatorTaught(f) && touchesAskedTerm(f))) {
|
|
6276
|
+
return null;
|
|
6277
|
+
}
|
|
6154
6278
|
// A remembered NEGATIVE is read on the same terms as the positive — it
|
|
6155
6279
|
// carries its own predicate and so never reaches ISA_PREDICATES.
|
|
6156
6280
|
const reply = isaPolarityReply(
|
|
@@ -6193,19 +6317,32 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6193
6317
|
return capabilityBaseRateReply(can[1], can[2], facts);
|
|
6194
6318
|
}
|
|
6195
6319
|
|
|
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
|
|
6320
|
+
// (b2b) "does a dog have a tail" — yes iff a remembered possession fact
|
|
6321
|
+
// says so: the forward yes/no mirror of WHAT_HAS_RE below, with the same
|
|
6198
6322
|
// single-hit lookup and "never a guessed no" discipline as CAN_ASK_RE
|
|
6199
6323
|
// above. Only diverts on a REAL hit, so a code-shaped "does app.mjs have
|
|
6200
|
-
// tests" (no
|
|
6324
|
+
// tests" (no possession fact) keeps whatever miss text already stands.
|
|
6325
|
+
// Both possession spellings are read (the corpus mints mgx:hasA, the teach
|
|
6326
|
+
// lane tmct:has), and the lookup lifts one taught ⊑-hop so "does rex have
|
|
6327
|
+
// fur" answers through "rex is a kind of dog; dog has fur", citing both.
|
|
6201
6328
|
const doesHave = q.match(DOES_HAVE_ASK_RE);
|
|
6202
6329
|
if (doesHave) {
|
|
6203
6330
|
const subj = factTermVariants(normFactTerm, doesHave[1]);
|
|
6204
6331
|
const obj = factTermVariants(normFactTerm, doesHave[2]);
|
|
6205
|
-
const
|
|
6206
|
-
|
|
6332
|
+
const HAS_PREDICATES = new Set(["mgx:hasA", "tmct:has"]);
|
|
6333
|
+
const facts = await memoryFacts(memoryDir);
|
|
6334
|
+
const hasHit = (subjectSet) => facts.find(
|
|
6335
|
+
(f) => HAS_PREDICATES.has(f.predicate) && subjectSet.has(f.subject) && obj.has(f.object),
|
|
6207
6336
|
);
|
|
6337
|
+
const hit = hasHit(subj);
|
|
6208
6338
|
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
6339
|
+
const isaStep = facts.find((f) => ISA_PREDICATES.has(f.predicate) && subj.has(f.subject));
|
|
6340
|
+
if (isaStep) {
|
|
6341
|
+
const lifted = hasHit(factTermVariants(normFactTerm, isaStep.object));
|
|
6342
|
+
if (lifted) {
|
|
6343
|
+
return { text: `yes — ${renderFactLine(isaStep)}; ${renderFactLine(lifted)}`, replace: true };
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6209
6346
|
return null;
|
|
6210
6347
|
}
|
|
6211
6348
|
|
|
@@ -6244,6 +6381,22 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6244
6381
|
}
|
|
6245
6382
|
const base = capabilityBaseRateReply(doAsk[2], doAsk[3], facts);
|
|
6246
6383
|
if (base) return base;
|
|
6384
|
+
// A subject NO fact row mentions on either side has nothing to answer
|
|
6385
|
+
// from at all — without this the turn fell through to the
|
|
6386
|
+
// conversational catch-all, which answered a question about penguins
|
|
6387
|
+
// with the identity blurb. Decline by name, with the round-trip teach
|
|
6388
|
+
// hint, and stay a miss. SINGLE-WORD subjects only: a multi-word
|
|
6389
|
+
// capture here is this loose shape misbinding a subject+verb ("does
|
|
6390
|
+
// margo eat ribs" reads [margo eat][ribs]), and a later reader owns
|
|
6391
|
+
// that sentence — the same keep-its-turn rule as the fall-through above.
|
|
6392
|
+
if (!/\s/.test(doAsk[2].trim()) && !facts.some((f) => subj.has(f.subject) || subj.has(f.object))) {
|
|
6393
|
+
const noun = singularizeSurface(teachableSubjectOf(doAsk[2]));
|
|
6394
|
+
return {
|
|
6395
|
+
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.`,
|
|
6396
|
+
replace: true,
|
|
6397
|
+
miss: true,
|
|
6398
|
+
};
|
|
6399
|
+
}
|
|
6247
6400
|
}
|
|
6248
6401
|
}
|
|
6249
6402
|
|
|
@@ -6551,7 +6704,11 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6551
6704
|
* pipeline even gets a look) would wrongly swallow that idiom as a
|
|
6552
6705
|
* vocabulary-term lookup for the literal term "in X". */
|
|
6553
6706
|
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;
|
|
6707
|
+
const WHAT_ELSE_ABOUT_RE = /^(?:what|anything)\s+else\s+(?:do\s+you\s+know\s+)?about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
6708
|
+
/** "what else can dogs do" — the capability spelling of the same beyond-the-
|
|
6709
|
+
* primary-answer question; the subject's remaining facts (capabilities
|
|
6710
|
+
* included) are the expansion it asks for. */
|
|
6711
|
+
const WHAT_ELSE_CAN_DO_RE = /^what\s+else\s+can\s+(?:an?\s+|the\s+)?(.+?)\s+do[?.!\s]*$/i;
|
|
6555
6712
|
/** The same question with its subject left implicit — "what else", "anything
|
|
6556
6713
|
* else", "what else do you know". A reader who has just been told about dogs
|
|
6557
6714
|
* and asks "what else" means "what else about dogs"; the subject is carried by
|
|
@@ -6581,7 +6738,7 @@ const WHAT_ELSE_BARE_RE = /^(?:(?:and|so|but|ok|okay|now|then)\s+)*(?:what\s+els
|
|
|
6581
6738
|
async function whatElseAnswer(memoryDir, query, last) {
|
|
6582
6739
|
if (!memoryDir) return null;
|
|
6583
6740
|
const q = String(query).trim();
|
|
6584
|
-
const m = q.match(WHAT_ELSE_IS_RE) || q.match(WHAT_ELSE_ABOUT_RE);
|
|
6741
|
+
const m = q.match(WHAT_ELSE_IS_RE) || q.match(WHAT_ELSE_ABOUT_RE) || q.match(WHAT_ELSE_CAN_DO_RE);
|
|
6585
6742
|
// A bare "what else" takes its subject from the standing referent — the same
|
|
6586
6743
|
// last-grounded-answer binding "can it bark" uses.
|
|
6587
6744
|
const bare = !m && WHAT_ELSE_BARE_RE.test(q);
|
|
@@ -7295,18 +7452,76 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7295
7452
|
const isaSubject = focusLabel && IS_ADJECTIVE_PRONOUN_RE.test(isaAsk[1].trim())
|
|
7296
7453
|
? focusLabel : isaAsk[1];
|
|
7297
7454
|
const subjCandidates = new Set(factTermVariants(normFactTerm, isaSubject));
|
|
7455
|
+
// REFLEXIVE subsumption — "is a dog a dog" holds by definition (⊑ is
|
|
7456
|
+
// reflexive, whatever the term); without this it fell to the can't-confirm
|
|
7457
|
+
// closer, which then offered to be taught "dog is a kind of dog".
|
|
7458
|
+
if ([...subjCandidates].some((s) => objVariants.has(s))) {
|
|
7459
|
+
const kindEcho = stripTrailingDiscourseTag(isaAsk[2]).trim();
|
|
7460
|
+
return {
|
|
7461
|
+
text: `yes — ${indefiniteArticleFor(kindEcho)} ${kindEcho} is ${indefiniteArticleFor(kindEcho)} ${kindEcho}, trivially: every kind is a kind of itself.`,
|
|
7462
|
+
replace: true,
|
|
7463
|
+
};
|
|
7464
|
+
}
|
|
7298
7465
|
const noun = await entityClassNoun(graph, isaSubject);
|
|
7299
7466
|
if (noun) for (const v of factTermVariants(normFactTerm, noun)) subjCandidates.add(v);
|
|
7467
|
+
const {
|
|
7468
|
+
findIsaChain, deriveDisjointViolations,
|
|
7469
|
+
SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE, DISJOINT_PREDICATE,
|
|
7470
|
+
} = await import("../domain/syllogise.mjs");
|
|
7471
|
+
const isTaught = isOperatorTaught;
|
|
7472
|
+
const chainSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE && isTaught(f));
|
|
7473
|
+
const chainTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE && isTaught(f));
|
|
7474
|
+
const chainSubClassEdges = chainSubClassRows.map((f) => [f.subject, f.object]);
|
|
7475
|
+
const chainTypeEdges = chainTypeRows.map((f) => [f.subject, f.object]);
|
|
7476
|
+
const mixedSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE);
|
|
7477
|
+
const mixedTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE);
|
|
7478
|
+
const mixedTypeEdges = mixedTypeRows.map((f) => [f.subject, f.object]);
|
|
7479
|
+
const mixedSubClassEdges = mixedSubClassRows.map((f) => [f.subject, f.object]);
|
|
7480
|
+
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
7481
|
+
const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
|
|
7482
|
+
// CAX-DW GATE, COMPUTED BEFORE ANY "YES" MAY RETURN: every taught
|
|
7483
|
+
// disjointness is lifted through the full ⊑-closure (subclass edges double
|
|
7484
|
+
// as type edges here, because an instance teach like "rex is a dog" stores
|
|
7485
|
+
// rdfs:subClassOf) and held against the asked conclusion. A "yes" whose
|
|
7486
|
+
// resolved chain crosses one of these would certify a stored
|
|
7487
|
+
// contradiction, so the gate runs ahead of the direct-fact verdict and
|
|
7488
|
+
// both proof chases below — never after them, where it can only lose.
|
|
7489
|
+
const disjointGateViolations = disjointRows.length
|
|
7490
|
+
? deriveDisjointViolations(
|
|
7491
|
+
mixedTypeEdges.concat(mixedSubClassEdges), mixedSubClassEdges, disjointEdges,
|
|
7492
|
+
{ budget: 20, focus: new Set([...subjCandidates, ...objVariants]) },
|
|
7493
|
+
)
|
|
7494
|
+
: [];
|
|
7495
|
+
const disjointRefusalFor = (subj) => {
|
|
7496
|
+
const v = disjointGateViolations.find((vv) => vv.subject === subj && objVariants.has(vv.object));
|
|
7497
|
+
if (!v) return null;
|
|
7498
|
+
const posFact = isa
|
|
7499
|
+
.filter((f) => objVariants.has(f.object) && (f.subject === v.viaClass || f.subject === v.subject))
|
|
7500
|
+
.sort(byTrust)[0];
|
|
7501
|
+
const disjointFact = disjointRows.find((f) => (f.subject === v.viaClass && f.object === v.object)
|
|
7502
|
+
|| (f.subject === v.object && f.object === v.viaClass));
|
|
7503
|
+
if (!posFact || !disjointFact) return null;
|
|
7504
|
+
return isaInconsistencyRefusal(posFact, disjointFact);
|
|
7505
|
+
};
|
|
7300
7506
|
const hit = isa
|
|
7301
7507
|
.filter((f) => subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7302
7508
|
.sort(byTrust)[0];
|
|
7303
7509
|
// A STORED NEGATIVE ("john is not a man") is a source disagreeing, so it is
|
|
7304
7510
|
// 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`.
|
|
7511
|
+
// default. It carries its own predicate and so never reaches `isa`. A
|
|
7512
|
+
// taught disjointness directly between the asked terms is the same
|
|
7513
|
+
// disagreement in owl:disjointWith spelling, so it reads as the negative
|
|
7514
|
+
// side on the same terms — ahead of every yes-chase, not after them.
|
|
7306
7515
|
const negHit = rows
|
|
7307
7516
|
.filter((f) => f.predicate === NEG_SUBCLASS_PREDICATE && subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7308
7517
|
.sort(byTrust)[0];
|
|
7309
|
-
const
|
|
7518
|
+
const directDisjoint = disjointRows.find((f) => (subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7519
|
+
|| (subjCandidates.has(f.object) && objVariants.has(f.subject)));
|
|
7520
|
+
if (hit && !negHit) {
|
|
7521
|
+
const chainRefusal = disjointRefusalFor(hit.subject);
|
|
7522
|
+
if (chainRefusal) return chainRefusal;
|
|
7523
|
+
}
|
|
7524
|
+
const polarityReply = isaPolarityReply(hit, negHit || directDisjoint);
|
|
7310
7525
|
if (polarityReply) return polarityReply;
|
|
7311
7526
|
// CLASS↔INSTANCE BRIDGE: when X resolves to a graph entity, its
|
|
7312
7527
|
// inherits chain's superclass LABELS are subject candidates too — a taught
|
|
@@ -7348,17 +7563,13 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7348
7563
|
// technically-true-per-ConceptNet "yes" that has nothing to do with
|
|
7349
7564
|
// what the OPERATOR taught; only operator/teach/entailed-sourced isa
|
|
7350
7565
|
// 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
7566
|
const factForStep = (step) => (step.predicate === SC_PREDICATE ? chainSubClassRows : chainTypeRows)
|
|
7358
7567
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
7359
7568
|
for (const subj of subjCandidates) {
|
|
7360
7569
|
const chain = findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassEdges, { maxHops: 2 });
|
|
7361
7570
|
if (!chain) continue;
|
|
7571
|
+
const chainRefusal = disjointRefusalFor(subj);
|
|
7572
|
+
if (chainRefusal) return chainRefusal;
|
|
7362
7573
|
const premises = chain.map(factForStep);
|
|
7363
7574
|
if (premises.every(Boolean)) return { text: `yes — ${renderIsaChain(premises)}`, replace: true };
|
|
7364
7575
|
}
|
|
@@ -7373,15 +7584,13 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7373
7584
|
// every premise is cited with its own source, corpus ones included. The
|
|
7374
7585
|
// shared taught-only rows above stay untouched: the disjoint and
|
|
7375
7586
|
// 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
7587
|
const mixedFactForStep = (step) => (step.predicate === SC_PREDICATE ? mixedSubClassRows : mixedTypeRows)
|
|
7379
7588
|
.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
7589
|
for (const subj of subjCandidates) {
|
|
7383
7590
|
const chain = findIsaChain(subj, objVariants, mixedTypeEdges, mixedSubClassEdges, { maxHops: 2 });
|
|
7384
7591
|
if (!chain) continue;
|
|
7592
|
+
const chainRefusal = disjointRefusalFor(subj);
|
|
7593
|
+
if (chainRefusal) return chainRefusal;
|
|
7385
7594
|
const premises = chain.map(mixedFactForStep);
|
|
7386
7595
|
if (premises.every(Boolean) && premises.some(isTaught)) {
|
|
7387
7596
|
return { text: `yes — ${renderIsaChain(premises)}`, replace: true };
|
|
@@ -7397,8 +7606,6 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7397
7606
|
// answer "no" from absence-of-membership rather than decline; anything
|
|
7398
7607
|
// this chase can't connect through a stated disjointness falls through
|
|
7399
7608
|
// 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
7609
|
// NEGATED membership — "is a dog not a cat". ISA_ASK_RE captures the
|
|
7403
7610
|
// subject as "dog not" (the "not" glues onto the subject because the
|
|
7404
7611
|
// article anchors the kind), so without this the negated question walks
|
|
@@ -7437,20 +7644,16 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7437
7644
|
};
|
|
7438
7645
|
}
|
|
7439
7646
|
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 });
|
|
7647
|
+
// Taught subclass edges double as type edges here, because an instance
|
|
7648
|
+
// teach ("felix is a cat") stores rdfs:subClassOf — without the fold the
|
|
7649
|
+
// instance form of the provable "no" never fired.
|
|
7650
|
+
const violations = deriveDisjointViolations(
|
|
7651
|
+
chainTypeEdges.concat(chainSubClassEdges), chainSubClassEdges, disjointEdges, { budget: 10 },
|
|
7652
|
+
);
|
|
7450
7653
|
for (const subj of subjCandidates) {
|
|
7451
7654
|
const v = violations.find((vv) => vv.subject === subj && objVariants.has(vv.object));
|
|
7452
7655
|
if (!v) continue;
|
|
7453
|
-
const typeFact = chainTypeRows.find((f) => f.subject === v.subject && f.object === v.viaType);
|
|
7656
|
+
const typeFact = chainTypeRows.concat(chainSubClassRows).find((f) => f.subject === v.subject && f.object === v.viaType);
|
|
7454
7657
|
const disjointFact = disjointRows.find((f) => (f.subject === v.viaClass && f.object === v.object)
|
|
7455
7658
|
|| (f.subject === v.object && f.object === v.viaClass));
|
|
7456
7659
|
const parts = [typeFact, disjointFact].filter(Boolean).map(renderFactLine);
|
|
@@ -7617,6 +7820,20 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7617
7820
|
miss: true, // still a MISS in the turn record — honest wording, not an answer
|
|
7618
7821
|
};
|
|
7619
7822
|
}
|
|
7823
|
+
// A stored CONVERSE ("every dog is a mammal" asked as "is a mammal a
|
|
7824
|
+
// dog") deserves better than the bare wall: name the direction that IS
|
|
7825
|
+
// known and why it doesn't answer. Still a miss, never a guessed "no" —
|
|
7826
|
+
// some mammals may well be dogs; the store just doesn't say.
|
|
7827
|
+
const converseHit = isa
|
|
7828
|
+
.filter((f) => subjCandidates.has(f.object) && objVariants.has(f.subject))
|
|
7829
|
+
.sort(byTrust)[0];
|
|
7830
|
+
if (converseHit) {
|
|
7831
|
+
return {
|
|
7832
|
+
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}".`,
|
|
7833
|
+
replace: true,
|
|
7834
|
+
miss: true,
|
|
7835
|
+
};
|
|
7836
|
+
}
|
|
7620
7837
|
// Subject with NO isa facts: only divert when it's mentioned NOWHERE at
|
|
7621
7838
|
// all (no fact row on either side, no code entity by id OR class noun) —
|
|
7622
7839
|
// a subject known via OTHER predicates ("ahab is male") or the code graph
|
|
@@ -7945,7 +8162,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7945
8162
|
// originally-intended case here) still gets this receipt exactly as
|
|
7946
8163
|
// before, since envelope.parsed is null for those adjectives.
|
|
7947
8164
|
if (rows.some(subjectMatch) && !envelope?.parsed) {
|
|
7948
|
-
return { text: `I don't have a fact saying ${
|
|
8165
|
+
return { text: `I don't have a fact saying ${suggestibleSubjectPhrase(subject)} is ${adjective}.`, replace: true };
|
|
7949
8166
|
}
|
|
7950
8167
|
// Without this, "is the checkout flow
|
|
7951
8168
|
// deprecated" as a genuinely FIRST-EVER question about a subject tmct
|
|
@@ -8282,7 +8499,17 @@ function discourseRewrite(query, last) {
|
|
|
8282
8499
|
} else {
|
|
8283
8500
|
const sm = String(query).match(STACCATO_SWAP_RE);
|
|
8284
8501
|
const cand = sm?.[1]?.trim();
|
|
8285
|
-
if (!cand
|
|
8502
|
+
if (!cand) return null;
|
|
8503
|
+
// VOCABULARY STACCATO: "tell me about a dog" -> "and a cat". The article
|
|
8504
|
+
// plus a plain word is the gate on the NEW term (a bare "and stuff" never
|
|
8505
|
+
// matches), and the PRIOR turn must itself have been a vocabulary
|
|
8506
|
+
// question — a code drill-down chain keeps the code-ish NAME_TOKEN rule
|
|
8507
|
+
// below unchanged.
|
|
8508
|
+
const articled = cand.match(/^(?:an?|the)\s+([a-z][\w-]*)$/i);
|
|
8509
|
+
const prevWasVocab = last?.query
|
|
8510
|
+
&& (BARE_WHATIS_RE.test(String(last.query)) || vagueTouchTermOf(String(last.query)));
|
|
8511
|
+
if (articled && prevWasVocab) return `what is a ${singularizeSurface(articled[1])}`;
|
|
8512
|
+
if (!NAME_TOKEN_RE.test(cand)) return null;
|
|
8286
8513
|
newSubj = cand;
|
|
8287
8514
|
}
|
|
8288
8515
|
if (!last?.query) return null;
|
|
@@ -8598,7 +8825,7 @@ function vagueTouchTermOf(query) {
|
|
|
8598
8825
|
q = q.replace(VAGUE_TOUCH_TEL_RE, "tell");
|
|
8599
8826
|
q = q.replace(VAGUE_TOUCH_ABUT_RE, "about");
|
|
8600
8827
|
q = applyPreambleFrames(q);
|
|
8601
|
-
const m = q.match(/^(?:kindly\s+)?tell me about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i)
|
|
8828
|
+
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
8829
|
|| 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
8830
|
// "explain X" — a bare "explain <term>" is at least as natural a vague touch as "tell
|
|
8604
8831
|
// me about X", but had no recognized shape at all: normalize.mjs's own
|
|
@@ -8687,7 +8914,7 @@ function relationTermOf(query, envelope) {
|
|
|
8687
8914
|
* "tell me" branch and a single external \s+ would double-count the
|
|
8688
8915
|
* separator when "about" fires. */
|
|
8689
8916
|
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;
|
|
8917
|
+
/^(?:(?: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
8918
|
|
|
8692
8919
|
/** Bare focus pronouns this lane resolves against the STANDING focus —
|
|
8693
8920
|
* "describe that" / "tell me about it" after a prior turn set the focus.
|
|
@@ -9147,15 +9374,28 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9147
9374
|
// the frame below once the store names the verb — same spec, same
|
|
9148
9375
|
// confirmation, same fold as its verbed twin.
|
|
9149
9376
|
let verblessGoal = goalMatch ? null : q.match(GOAL_TEACH_VERBLESS_RE);
|
|
9150
|
-
//
|
|
9151
|
-
//
|
|
9377
|
+
// A conjunction of goal atoms ("the goal is that disk-1 rests on peg-b and
|
|
9378
|
+
// disk-3 rests on peg-c") — every conjunct must compile, else the single-goal
|
|
9379
|
+
// frames (and their honest declines) keep their turn.
|
|
9380
|
+
let conjunctMatches = null;
|
|
9381
|
+
if (!goalMatch && !verblessGoal) {
|
|
9382
|
+
const conj = q.match(GOAL_TEACH_CONJUNCTION_RE);
|
|
9383
|
+
if (conj && /\s+and\s+/i.test(conj[1])) {
|
|
9384
|
+
const parts = conj[1].split(/\s+and\s+/i).map((p) => p.trim());
|
|
9385
|
+
const matched = parts.map((p) => p.match(GOAL_CONJUNCT_RE));
|
|
9386
|
+
if (parts.length > 1 && matched.every(Boolean)) conjunctMatches = matched;
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
// The imperative voicing ("get all the disks onto peg-c") and the bare-NP
|
|
9390
|
+
// voicing ("the goal is all disks on peg-c") fold into the same verbless
|
|
9391
|
+
// resolution: singularize the class term ("disks"→"disk"), read the
|
|
9152
9392
|
// universal off the quantifier, and normalize the motion preposition to the
|
|
9153
9393
|
// 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 = [
|
|
9394
|
+
if (!goalMatch && !verblessGoal && !conjunctMatches) {
|
|
9395
|
+
const bare = q.match(GOAL_TEACH_NP_RE) || q.match(GOAL_TEACH_IMPERATIVE_RE);
|
|
9396
|
+
if (bare) {
|
|
9397
|
+
const prep = { onto: "on", into: "in", upon: "on" }[bare[3].toLowerCase()] ?? bare[3].toLowerCase();
|
|
9398
|
+
verblessGoal = [bare[0], bare[1] ? "every" : "", singularizeSurface(bare[2]), prep, bare[4]];
|
|
9159
9399
|
}
|
|
9160
9400
|
}
|
|
9161
9401
|
if (verblessGoal) {
|
|
@@ -9178,29 +9418,35 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9178
9418
|
}
|
|
9179
9419
|
goalMatch = [verblessGoal[0], verblessGoal[1], verblessGoal[2], verbs[0], verblessGoal[3], verblessGoal[4]];
|
|
9180
9420
|
}
|
|
9181
|
-
if (goalMatch) {
|
|
9421
|
+
if (goalMatch || conjunctMatches) {
|
|
9182
9422
|
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
9183
|
-
const
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9423
|
+
const items = conjunctMatches ?? [goalMatch];
|
|
9424
|
+
const specs = [];
|
|
9425
|
+
const tails = [];
|
|
9426
|
+
for (const m of items) {
|
|
9427
|
+
const verb = await verbLemma(m[3]);
|
|
9428
|
+
if (!verb) {
|
|
9429
|
+
return {
|
|
9430
|
+
text: `I can't reduce "${m[3]}" to a verb for that goal — try the plain form (e.g. "rests").`,
|
|
9431
|
+
via: "plan", deduced: "record the goal state for a later plan", note: "GOAL frame — verb lemma unavailable, honest decline",
|
|
9432
|
+
};
|
|
9433
|
+
}
|
|
9434
|
+
specs.push({
|
|
9435
|
+
universal: !!m[1],
|
|
9436
|
+
term: normFactTerm(m[2]),
|
|
9437
|
+
predicate: `${verb}-${m[4].toLowerCase()}`,
|
|
9438
|
+
object: normFactTerm(m[5]),
|
|
9439
|
+
});
|
|
9440
|
+
// A conjunct restates itself; the that-form keeps its own words; the
|
|
9441
|
+
// infinitive/verbless voicings restate as the that-form, so the goal
|
|
9442
|
+
// check's own "done — …" line and the confirmation read identically.
|
|
9443
|
+
tails.push(conjunctMatches
|
|
9444
|
+
? `${m[1] ? `${m[1].toLowerCase()} ` : ""}${m[2].toLowerCase()} ${m[3].toLowerCase()} ${m[4].toLowerCase()} ${m[5].toLowerCase()}`
|
|
9445
|
+
: (thatGoal
|
|
9446
|
+
? q.replace(/^the\s+goal\s+is\s+that\s+/i, "").replace(/[.!?]+$/, "")
|
|
9447
|
+
: `${m[1] ? `${m[1].toLowerCase()} ` : ""}${m[2].toLowerCase()} ${verb}s ${m[4].toLowerCase()} ${m[5].toLowerCase()}`));
|
|
9189
9448
|
}
|
|
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
9449
|
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
9450
|
// Restating a goal you already set is one goal, not two. The spec is four
|
|
9205
9451
|
// normalized scalars, so the same goal in either voicing ("the goal is
|
|
9206
9452
|
// that …" / "the goal is to …") compiles to the identical object and a
|
|
@@ -9211,19 +9457,26 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9211
9457
|
// goals and goalTexts move in LOCKSTEP — "solve it" joins goalTexts by
|
|
9212
9458
|
// index to describe the specs it compiled, so dropping one without the
|
|
9213
9459
|
// other misaligns the plan's own account of what it is solving for.
|
|
9214
|
-
|
|
9460
|
+
let heldGoals = prev?.goals ?? [];
|
|
9461
|
+
let heldTexts = prev?.goalTexts ?? [];
|
|
9462
|
+
let added = 0;
|
|
9463
|
+
for (let i = 0; i < specs.length; i += 1) {
|
|
9464
|
+
if (heldGoals.some((g) => sameGoalSpec(g, specs[i]))) continue;
|
|
9465
|
+
heldGoals = [...heldGoals, specs[i]];
|
|
9466
|
+
heldTexts = [...heldTexts, tails[i]];
|
|
9467
|
+
added += 1;
|
|
9468
|
+
}
|
|
9215
9469
|
planHolder.state = {
|
|
9216
|
-
goals:
|
|
9217
|
-
goalTexts: alreadyHeld ? heldTexts : [...heldTexts, tail],
|
|
9470
|
+
goals: heldGoals, goalTexts: heldTexts,
|
|
9218
9471
|
actions: null, states: null, stepGoals: null, cursor: 0, done: false,
|
|
9219
9472
|
};
|
|
9220
|
-
const n =
|
|
9473
|
+
const n = heldGoals.length;
|
|
9221
9474
|
return {
|
|
9222
|
-
text: `${
|
|
9475
|
+
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.`,
|
|
9223
9476
|
via: "plan", deduced: "record the goal state for a later plan",
|
|
9224
|
-
note:
|
|
9225
|
-
?
|
|
9226
|
-
: "GOAL frame — goal spec
|
|
9477
|
+
note: added
|
|
9478
|
+
? `GOAL frame — ${added === 1 ? "goal spec" : `${added} goal specs`} accumulated on the session plan slot`
|
|
9479
|
+
: "GOAL frame — the same goal spec was already held, so it folded onto the existing one",
|
|
9227
9480
|
};
|
|
9228
9481
|
}
|
|
9229
9482
|
|
|
@@ -9282,6 +9535,24 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9282
9535
|
}
|
|
9283
9536
|
const goals = planHolder.state.goals;
|
|
9284
9537
|
const goalText = planHolder.state.goalTexts.join("; ");
|
|
9538
|
+
// A goal naming a term the board and the taught classes have never heard of
|
|
9539
|
+
// ("peg-z") can never be reached — decline by name BEFORE the search, so an
|
|
9540
|
+
// unknown token is a named miss rather than a full-depth search burn.
|
|
9541
|
+
const knownTerms = new Set([
|
|
9542
|
+
...Object.keys(domain.classMembers || {}),
|
|
9543
|
+
...Object.values(domain.classMembers || {}).flat(),
|
|
9544
|
+
...state.flatMap((r) => [r.subject, r.object]),
|
|
9545
|
+
]);
|
|
9546
|
+
const unknownGoalTerms = [...new Set(goals.flatMap((g) => [g.term, g.object]))]
|
|
9547
|
+
.filter((t) => t && !knownTerms.has(t));
|
|
9548
|
+
if (unknownGoalTerms.length) {
|
|
9549
|
+
const quoted = unknownGoalTerms.map((t) => `"${t}"`).join(" and ");
|
|
9550
|
+
return {
|
|
9551
|
+
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").`,
|
|
9552
|
+
via: "plan", deduced: "plan a move sequence (unknown goal term)",
|
|
9553
|
+
note: "plan lane — honest decline: the goal names an untaught term, search never started",
|
|
9554
|
+
};
|
|
9555
|
+
}
|
|
9285
9556
|
let isGoal;
|
|
9286
9557
|
try {
|
|
9287
9558
|
isGoal = compileGoal(goals, domain);
|
|
@@ -9327,12 +9598,20 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
9327
9598
|
};
|
|
9328
9599
|
const ruleNames = [...new Set(domain.actions.map((a) => a.name))].join('", "');
|
|
9329
9600
|
const moveLines = actions.map((a, i) => ` ${i + 1}. ${a.label}`);
|
|
9601
|
+
// A piece the goal reaches for with no taught position is an ASSUMPTION the
|
|
9602
|
+
// plan silently makes (it reads the board as taught, without that piece) —
|
|
9603
|
+
// said out loud with the plan rather than left implicit.
|
|
9604
|
+
const goalPieces = [...new Set(goals.flatMap((g) => (g.universal ? (domain.classMembers?.[g.term] || []) : [g.term])))];
|
|
9605
|
+
const unplacedPieces = goalPieces.filter((p) => !state.some((r) => r.subject === p));
|
|
9606
|
+
const assumptionNote = unplacedPieces.length
|
|
9607
|
+
? `\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.`
|
|
9608
|
+
: "";
|
|
9330
9609
|
const text = n === 0
|
|
9331
|
-
? `the goal already holds — nothing to do
|
|
9610
|
+
? `the goal already holds — nothing to do.${assumptionNote}`
|
|
9332
9611
|
: `plan found — ${n} move${n === 1 ? "" : "s"} (shortest):\n${moveLines.join("\n")}\n\n` +
|
|
9333
9612
|
`because — you taught me the "${ruleNames}" rule${domain.actions.length === 1 ? "" : "s"}` +
|
|
9334
9613
|
`${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"
|
|
9614
|
+
`Say "next" to make move 1, or ask "what moves are legal now".${assumptionNote}`;
|
|
9336
9615
|
return {
|
|
9337
9616
|
text, via: "plan",
|
|
9338
9617
|
deduced: `plan a move sequence from the current state to the goal (${n} move${n === 1 ? "" : "s"})`,
|
|
@@ -9419,8 +9698,10 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder }) {
|
|
|
9419
9698
|
const clear = q.match(IS_CLEAR_RE);
|
|
9420
9699
|
const rev = clear ? null : q.match(BOARD_REVERSE_LOC_RE);
|
|
9421
9700
|
const fwd = clear || rev ? null : q.match(BOARD_FORWARD_LOC_RE);
|
|
9422
|
-
const
|
|
9423
|
-
|
|
9701
|
+
const whereEvery = clear || rev || fwd ? null : q.match(BOARD_WHERE_EVERY_RE);
|
|
9702
|
+
const where = clear || rev || fwd || whereEvery ? null
|
|
9703
|
+
: (q.match(BOARD_WHERE_RE) || q.match(BOARD_WHERE_DOES_RE));
|
|
9704
|
+
if (!clear && !rev && !fwd && !where && !whereEvery) return null;
|
|
9424
9705
|
if (!memoryDir) return null;
|
|
9425
9706
|
|
|
9426
9707
|
let ctx;
|
|
@@ -9438,6 +9719,20 @@ async function planFollowUpAnswer(query, { memoryDir, planHolder }) {
|
|
|
9438
9719
|
? { 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
9720
|
: { 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
9721
|
}
|
|
9722
|
+
if (whereEvery) {
|
|
9723
|
+
const cls = normFactTerm(singularizeSurface(whereEvery[1]));
|
|
9724
|
+
const members = domain.classMembers?.[cls] || [];
|
|
9725
|
+
if (!members.length) return null; // not a taught class — the ordinary readers decide
|
|
9726
|
+
const lines = members.map((mbr) => {
|
|
9727
|
+
const rows = state.filter((r) => r.subject === mbr);
|
|
9728
|
+
return rows.length ? rows.map(factPhrase).join("; ") : `nothing on the current board says where ${mbr} is`;
|
|
9729
|
+
});
|
|
9730
|
+
return {
|
|
9731
|
+
text: lines.join("\n"),
|
|
9732
|
+
deduced: "read the current board (where every member of a class is)",
|
|
9733
|
+
note: "BOARD — forward locative for every member of the taught class",
|
|
9734
|
+
};
|
|
9735
|
+
}
|
|
9441
9736
|
if (where || fwd) {
|
|
9442
9737
|
const x = normFactTerm((where ?? fwd)[1]);
|
|
9443
9738
|
if (!individuals.has(x)) return null;
|
|
@@ -9710,7 +10005,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9710
10005
|
if (miss) {
|
|
9711
10006
|
const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint, focus });
|
|
9712
10007
|
if (meta) {
|
|
9713
|
-
|
|
10008
|
+
// A lane may answer with a better-worded decline (the module-orient
|
|
10009
|
+
// residue guard) — still a miss in the turn record, like the isa
|
|
10010
|
+
// ladder's own closers.
|
|
10011
|
+
answer = meta.text; via = meta.via; recordMiss = meta.miss ?? false; handled = true;
|
|
9714
10012
|
note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
|
|
9715
10013
|
}
|
|
9716
10014
|
}
|
|
@@ -10519,8 +10817,8 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
10519
10817
|
return mk(`narrate mode ${next ? "on" : "off"}.`, { narrateNext: next });
|
|
10520
10818
|
}
|
|
10521
10819
|
|
|
10522
|
-
// /memory [verbose] — what tmct remembers, as text (the
|
|
10523
|
-
//
|
|
10820
|
+
// /memory [verbose] — what tmct remembers, as text (the same renderer
|
|
10821
|
+
// serves the `tmct memory` CLI).
|
|
10524
10822
|
if (name === "memory") {
|
|
10525
10823
|
note(trace, "goal: inspect tmct's memory store (facts/utterances/sessions)");
|
|
10526
10824
|
if (!memoryDir) return mk("no memory store here — /memory works inside a repo session.", { miss: true });
|
|
@@ -10916,21 +11214,63 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
10916
11214
|
const PAGE = 32;
|
|
10917
11215
|
const MORE_RE = /^(?:more|show more|see more|the rest|next|continue|go on)\b[.!?]*$/i;
|
|
10918
11216
|
|
|
10919
|
-
/** "what would break if I change X"
|
|
10920
|
-
*
|
|
10921
|
-
* COUNTERFACTUAL_RE ("if X were deleted, what would break"), which
|
|
10922
|
-
*
|
|
10923
|
-
*
|
|
10924
|
-
*
|
|
10925
|
-
*
|
|
11217
|
+
/** The impact-intent gate — "what would break if I change X" and its natural
|
|
11218
|
+
* neighbours, routed to the same /impact closure. Sibling of normalize.mjs's
|
|
11219
|
+
* COUNTERFACTUAL_RE ("if X were deleted, what would break"), which compiles
|
|
11220
|
+
* to the reverse import closure; these shapes name a CHANGE rather than a
|
|
11221
|
+
* deletion, so they answer with the impact closure /impact itself renders.
|
|
11222
|
+
* The verbs are a closed set on both sides — no general "any verb in a
|
|
11223
|
+
* conditional" fit. The gate runs ahead of the teach classifier and the
|
|
11224
|
+
* relaxation cascade, because an interrogative must never reach the write
|
|
11225
|
+
* boundary ("blast radius of X" was remembered as a fact) and "impact" must
|
|
11226
|
+
* never be fuzzy-read as "import" (the inverse question). */
|
|
11227
|
+
const IMPACT_CHANGE_VERBS = "(?:changed?|modif(?:y|ied)|edits?|edited|touch(?:es|ed)?|updates?|updated|alters?|altered|deletes?|deleted|removes?|removed|drops?|dropped)";
|
|
10926
11228
|
const IMPACT_PARAPHRASE_RE = new RegExp(
|
|
10927
11229
|
"^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)"
|
|
11230
|
+
+ "(?:breaks?|fails?|happens?|stops?\\s+working|is\\s+affected|are\\s+affected|gets?\\s+affected|be\\s+affected|is\\s+impacted|be\\s+impacted)"
|
|
11231
|
+
+ `\\s+if\\s+(?:i|we|you|one|someone)\\s+${IMPACT_CHANGE_VERBS}`
|
|
10931
11232
|
+ "\\s+(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
10932
11233
|
"i",
|
|
10933
11234
|
);
|
|
11235
|
+
// The same counterfactual with the clauses reversed — "if I change X what breaks".
|
|
11236
|
+
const IMPACT_REVERSED_RE = new RegExp(
|
|
11237
|
+
`^if\\s+(?:i|we|you|one|someone)\\s+${IMPACT_CHANGE_VERBS}`
|
|
11238
|
+
+ "\\s+(?:the\\s+)?(.+?),?\\s+what\\s+(?:would\\s+|will\\s+|might\\s+|could\\s+|does\\s+|do\\s+)?"
|
|
11239
|
+
+ "(?:breaks?|fails?|happens?|stops?\\s+working|is\\s+affected|are\\s+affected|gets?\\s+affected|be\\s+affected|would\\s+break|will\\s+break)"
|
|
11240
|
+
+ "[?.!\\s]*$",
|
|
11241
|
+
"i",
|
|
11242
|
+
);
|
|
11243
|
+
// The agentless passive — "what is affected by changing X".
|
|
11244
|
+
const IMPACT_AFFECTED_BY_RE = new RegExp(
|
|
11245
|
+
"^what\\s+(?:is|are|gets?|would\\s+be|will\\s+be)\\s+(?:affected|impacted|broken)\\s+"
|
|
11246
|
+
+ "(?:by|when|if)\\s+(?:i\\s+|we\\s+|you\\s+)?"
|
|
11247
|
+
+ "(?: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+"
|
|
11248
|
+
+ "(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
11249
|
+
"i",
|
|
11250
|
+
);
|
|
11251
|
+
// "can I safely delete X" — a change-safety question IS the impact question.
|
|
11252
|
+
const IMPACT_SAFE_CHANGE_RE = new RegExp(
|
|
11253
|
+
"^(?:(?:can|could)\\s+(?:i|we|you|one|someone)\\s+safely|is\\s+it\\s+safe\\s+to)\\s+"
|
|
11254
|
+
+ "(?:change|edit|modify|touch|update|alter|delete|remove|drop)\\s+"
|
|
11255
|
+
+ "(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
11256
|
+
"i",
|
|
11257
|
+
);
|
|
11258
|
+
// The NP form — "blast radius of X", "impact of changing X".
|
|
11259
|
+
const IMPACT_NOUN_RE = new RegExp(
|
|
11260
|
+
"^(?:what(?:'s|\\s+is)\\s+the\\s+)?(?:blast\\s+radius|impact)\\s+(?:of|for)\\s+"
|
|
11261
|
+
+ "(?:chang(?:ing|es)\\s+|editing\\s+|modifying\\s+|touching\\s+|updating\\s+|deleting\\s+|removing\\s+)?"
|
|
11262
|
+
+ "(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
11263
|
+
"i",
|
|
11264
|
+
);
|
|
11265
|
+
const IMPACT_INTENT_RES = [IMPACT_PARAPHRASE_RE, IMPACT_REVERSED_RE, IMPACT_AFFECTED_BY_RE, IMPACT_SAFE_CHANGE_RE, IMPACT_NOUN_RE];
|
|
11266
|
+
/** The impact intent in any of its clause orders -> the subject term, or null. */
|
|
11267
|
+
function matchImpactIntent(line) {
|
|
11268
|
+
for (const re of IMPACT_INTENT_RES) {
|
|
11269
|
+
const m = line.match(re);
|
|
11270
|
+
if (m) return m[1].trim();
|
|
11271
|
+
}
|
|
11272
|
+
return null;
|
|
11273
|
+
}
|
|
10934
11274
|
const joinList = (a) => (a.length > 1 ? `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}` : (a[0] ?? ""));
|
|
10935
11275
|
|
|
10936
11276
|
/** Render the next page of a held remainder (pending: {items:[str], noun}). Returns a
|
|
@@ -10956,6 +11296,40 @@ function morePage(query, { last, focus }) {
|
|
|
10956
11296
|
// throughout this file, and centralizing it risks double-processing.
|
|
10957
11297
|
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
11298
|
|
|
11299
|
+
// First-person desire openers for a vocabulary question — "i wanna know about
|
|
11300
|
+
// a horse", "you tell me about dog", "let me know about a dog" — and the
|
|
11301
|
+
// known-kinds enumeration ("what animals do you know", "list the animals you
|
|
11302
|
+
// know"). Each rewrites to the canonical question its lane already answers
|
|
11303
|
+
// ("tell me about X" / "what is a X"), BEFORE any dispatch lane sees the
|
|
11304
|
+
// text: the leading "i" otherwise reads as a teach subject, so a read-only
|
|
11305
|
+
// question asserted an intent it doesn't have. Closed set, rewrite-only —
|
|
11306
|
+
// same discipline as INDIRECT_REQUEST_RE above.
|
|
11307
|
+
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;
|
|
11308
|
+
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;
|
|
11309
|
+
// "facts"/"things"/"stuff" ask for the whole-store recall, not a kind's
|
|
11310
|
+
// members — those keep their own lane.
|
|
11311
|
+
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;
|
|
11312
|
+
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;
|
|
11313
|
+
// "if something is a dog then it is a pet" — the universal conditional IS the
|
|
11314
|
+
// universal subclass teach in a conditional coat, so it rewrites to the
|
|
11315
|
+
// "every X is a Y" surface the teach path already stores (with its quantifier
|
|
11316
|
+
// and its own confirmation). Closed to the indefinite-pronoun subject: a
|
|
11317
|
+
// conditional over a NAMED subject or an arbitrary property is a rule, not a
|
|
11318
|
+
// subclass fact, and stays outside this frame.
|
|
11319
|
+
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;
|
|
11320
|
+
function rewriteVocabOpener(line) {
|
|
11321
|
+
let m = line.match(DESIRE_ABOUT_RE) || line.match(TELL_ABOUT_VARIANT_RE);
|
|
11322
|
+
if (m) return `tell me about ${m[1].trim()}`;
|
|
11323
|
+
m = line.match(UNIVERSAL_CONDITIONAL_RE);
|
|
11324
|
+
if (m) return `every ${m[1]} is ${indefiniteArticleFor(m[2])} ${m[2]}`;
|
|
11325
|
+
m = line.match(KNOWN_KINDS_RE) || line.match(LIST_KNOWN_KINDS_RE);
|
|
11326
|
+
if (m) {
|
|
11327
|
+
const noun = teachableSubjectOf(m[1]);
|
|
11328
|
+
return `what is ${indefiniteArticleFor(noun)} ${noun}`;
|
|
11329
|
+
}
|
|
11330
|
+
return null;
|
|
11331
|
+
}
|
|
11332
|
+
|
|
10959
11333
|
/** A DISCONTIGUOUS verb frame, "SUBJECT uses OBJECT as its/a base(class)" —
|
|
10960
11334
|
* "uses" is split from its own qualifier ("as its base") around the object,
|
|
10961
11335
|
* so no contiguous phrase-table entry could ever register it, and "uses"
|
|
@@ -11080,7 +11454,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11080
11454
|
// the ORIGINAL `line` survives untouched for record.query/logLines fidelity
|
|
11081
11455
|
// — restored centrally inside withLast (below), once, for every dispatch path.
|
|
11082
11456
|
const indirectMatch = line.match(INDIRECT_REQUEST_RE);
|
|
11083
|
-
const
|
|
11457
|
+
const indirectLine = indirectMatch ? indirectMatch[1].trim() : line;
|
|
11458
|
+
const preRewriteLine = rewriteVocabOpener(indirectLine) || indirectLine;
|
|
11084
11459
|
// rewriteUsesAsBaseFrame's discontiguous-frame rewrite: applied here, once,
|
|
11085
11460
|
// before ANY dispatch lane sees the text. Null (no-op) for every turn that
|
|
11086
11461
|
// doesn't match one of the four discontiguous shapes.
|
|
@@ -11214,20 +11589,21 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
11214
11589
|
return withLast(morePage(workingLine, ctx), "continue viewing a previous long listing");
|
|
11215
11590
|
}
|
|
11216
11591
|
|
|
11217
|
-
// "what would break if I change X" / "
|
|
11592
|
+
// "what would break if I change X" / "if I change X what breaks" / "blast
|
|
11593
|
+
// radius of X" / "impact of X" / "can I safely delete X" — the impact
|
|
11218
11594
|
// 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 (
|
|
11595
|
+
// own each of these reached a wrong lane: the forward form's residue
|
|
11596
|
+
// ("break I") read as a subject for the history lane's `touches`, the NP
|
|
11597
|
+
// form fell to the teach lane (a read-only question mutating memory) or to
|
|
11598
|
+
// the fuzzy corrector ("impact" read as "import", the inverse question).
|
|
11599
|
+
// /impact's own closure is the answer, and its wording ("Impact of changing
|
|
11600
|
+
// X") already says the change is hypothetical.
|
|
11601
|
+
const impactSubject = matchImpactIntent(workingLine);
|
|
11602
|
+
if (impactSubject) {
|
|
11227
11603
|
const impactDeduced = "understand what a change to this module would reach (impact closure)";
|
|
11228
11604
|
note(trace, `goal: ${impactDeduced}`);
|
|
11229
|
-
note(trace, `lane:
|
|
11230
|
-
return withLast(await runCommand(`/impact ${
|
|
11605
|
+
note(trace, `lane: impact intent matched -> /impact ${impactSubject}`);
|
|
11606
|
+
return withLast(await runCommand(`/impact ${impactSubject}`, ctx), impactDeduced);
|
|
11231
11607
|
}
|
|
11232
11608
|
|
|
11233
11609
|
// Multi-sentence pre-split — one message carrying several sentences
|