@polycode-projects/the-mechanical-code-talker 2.3.0 → 2.5.0
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 +127 -28
- package/ROADMAP.md +3 -2
- package/bin/tmct.mjs +17 -90
- package/corpus/LICENSES.json +19 -4
- package/corpus/README.md +48 -0
- package/corpus/generated/README.md +24 -9
- package/corpus/generated/ace-surface-variants.jsonl +4 -1
- package/corpus/generated/manifest.json +4 -4
- package/corpus/prose/manifest.json +512 -0
- package/corpus/prose/sqlite/LICENSE-NOTICE +53 -0
- package/corpus/prose/sqlite/arch.txt +213 -0
- package/corpus/prose/sqlite/atomiccommit.txt +1117 -0
- package/corpus/prose/sqlite/faq.txt +473 -0
- package/corpus/prose/sqlite/fileformat.txt +1589 -0
- package/corpus/prose/sqlite/lang_createtable.txt +1339 -0
- package/corpus/prose/sqlite/lang_insert.txt +580 -0
- package/corpus/prose/sqlite/lang_select.txt +3293 -0
- package/corpus/prose/sqlite/optoverview.txt +908 -0
- package/corpus/prose/sqlite/queryplanner.txt +447 -0
- package/corpus/prose/sqlite/transactional.txt +41 -0
- package/corpus/prose/sqlite/wal.txt +567 -0
- package/corpus/prose/sqlite/whentouse.txt +300 -0
- package/corpus/prose/wikipedia/Apple.txt +4 -0
- package/corpus/prose/wikipedia/Attempto_Controlled_English.txt +169 -0
- package/corpus/prose/wikipedia/Automated_planning_and_scheduling.txt +67 -0
- package/corpus/prose/wikipedia/Bee.txt +7 -0
- package/corpus/prose/wikipedia/Bird.txt +8 -0
- package/corpus/prose/wikipedia/Bone.txt +4 -0
- package/corpus/prose/wikipedia/Book.txt +7 -0
- package/corpus/prose/wikipedia/Bread.txt +6 -0
- package/corpus/prose/wikipedia/Butterfly.txt +6 -0
- package/corpus/prose/wikipedia/Car.txt +1 -0
- package/corpus/prose/wikipedia/Cat.txt +1 -0
- package/corpus/prose/wikipedia/Child.txt +3 -0
- package/corpus/prose/wikipedia/City.txt +2 -0
- package/corpus/prose/wikipedia/Clock.txt +2 -0
- package/corpus/prose/wikipedia/Cooking.txt +1 -0
- package/corpus/prose/wikipedia/Description_logic.txt +660 -0
- package/corpus/prose/wikipedia/Doctor.txt +6 -0
- package/corpus/prose/wikipedia/Dog.txt +4 -0
- package/corpus/prose/wikipedia/Eagle.txt +4 -0
- package/corpus/prose/wikipedia/Emotion.txt +9 -0
- package/corpus/prose/wikipedia/Eye.txt +5 -0
- package/corpus/prose/wikipedia/Family.txt +3 -0
- package/corpus/prose/wikipedia/Farm.txt +4 -0
- package/corpus/prose/wikipedia/Fear.txt +4 -0
- package/corpus/prose/wikipedia/First-order_logic.txt +1518 -0
- package/corpus/prose/wikipedia/Fish.txt +10 -0
- package/corpus/prose/wikipedia/Flower.txt +3 -0
- package/corpus/prose/wikipedia/Food.txt +10 -0
- package/corpus/prose/wikipedia/Grass.txt +9 -0
- package/corpus/prose/wikipedia/Hand.txt +2 -0
- package/corpus/prose/wikipedia/Happiness.txt +3 -0
- package/corpus/prose/wikipedia/Heart.txt +4 -0
- package/corpus/prose/wikipedia/Horse.txt +4 -0
- package/corpus/prose/wikipedia/House.txt +6 -0
- package/corpus/prose/wikipedia/Human.txt +4 -0
- package/corpus/prose/wikipedia/Insect.txt +6 -0
- package/corpus/prose/wikipedia/Interactive_fiction.txt +112 -0
- package/corpus/prose/wikipedia/Knowledge.txt +5 -0
- package/corpus/prose/wikipedia/Knowledge_representation_and_reasoning.txt +87 -0
- package/corpus/prose/wikipedia/LICENSE-NOTICE +94 -0
- package/corpus/prose/wikipedia/Language.txt +10 -0
- package/corpus/prose/wikipedia/Learning.txt +4 -0
- package/corpus/prose/wikipedia/Mammal.txt +3 -0
- package/corpus/prose/wikipedia/Memory.txt +5 -0
- package/corpus/prose/wikipedia/Milk.txt +1 -0
- package/corpus/prose/wikipedia/Mountain.txt +1 -0
- package/corpus/prose/wikipedia/Natural_language_processing.txt +211 -0
- package/corpus/prose/wikipedia/Ostrich.txt +2 -0
- package/corpus/prose/wikipedia/Owl.txt +2 -0
- package/corpus/prose/wikipedia/Penguin.txt +2 -0
- package/corpus/prose/wikipedia/Plant.txt +5 -0
- package/corpus/prose/wikipedia/Rain.txt +1 -0
- package/corpus/prose/wikipedia/Resource_Description_Framework.txt +184 -0
- package/corpus/prose/wikipedia/River.txt +1 -0
- package/corpus/prose/wikipedia/School.txt +8 -0
- package/corpus/prose/wikipedia/Sea.txt +1 -0
- package/corpus/prose/wikipedia/Semantic_Web.txt +114 -0
- package/corpus/prose/wikipedia/Semantic_reasoner.txt +29 -0
- package/corpus/prose/wikipedia/Snow.txt +5 -0
- package/corpus/prose/wikipedia/Sun.txt +5 -0
- package/corpus/prose/wikipedia/Teacher.txt +4 -0
- package/corpus/prose/wikipedia/Team.txt +3 -0
- package/corpus/prose/wikipedia/Text-based_game.txt +17 -0
- package/corpus/prose/wikipedia/Tool.txt +4 -0
- package/corpus/prose/wikipedia/Tree.txt +7 -0
- package/corpus/prose/wikipedia/Weather.txt +4 -0
- package/corpus/prose/wikipedia/Web_Ontology_Language.txt +133 -0
- package/corpus/prose/wikipedia/Wind.txt +8 -0
- package/corpus/prose/wikipedia/Writing.txt +5 -0
- package/corpus/seon/README.md +1 -0
- package/corpus/tier2/generate.mjs +18 -18
- package/corpus/tier2/manifest.json +3 -3
- package/data/games/hanoi-3.txt +8 -2
- package/package.json +24 -5
- package/src/adapters/corpus-lanes.mjs +13 -0
- package/src/adapters/graph-build.mjs +5 -7
- package/src/adapters/import-closure.mjs +28 -0
- package/src/adapters/memory/blocks.mjs +5 -4
- package/src/adapters/memory/core.mjs +78 -5
- package/src/adapters/memory/shacl.mjs +12 -0
- package/src/adapters/providers/graph-service.mjs +12 -5
- package/src/adapters/tracked-files.mjs +17 -0
- package/src/domain/ask-vocab.mjs +2 -0
- package/src/domain/ask.mjs +225 -13
- package/src/domain/cli-verbs.mjs +201 -0
- package/src/domain/codegraph.mjs +142 -56
- package/src/domain/completions/graph-adapter.mjs +1 -1
- package/src/domain/completions/group.mjs +3 -17
- package/src/domain/completions/infer.mjs +4 -13
- package/src/domain/completions/rank.mjs +6 -19
- package/src/domain/grammar/lexicon-core.json +1 -1
- package/src/domain/hash.mjs +36 -13
- package/src/domain/interpret/fuzzy.mjs +7 -2
- package/src/domain/interpret/normalize.mjs +9 -0
- package/src/domain/interpret/strategies/keywords.mjs +19 -9
- package/src/domain/memory/capability.mjs +22 -3
- package/src/domain/memory/touched-facts.mjs +17 -0
- package/src/domain/module-paths.mjs +9 -0
- package/src/domain/persona/tiers.mjs +1 -1
- package/src/domain/planning.mjs +37 -0
- package/src/domain/prose.mjs +10 -2
- package/src/domain/relative-specifiers.mjs +12 -0
- package/src/domain/router/registry.mjs +3 -2
- package/src/domain/router/results.mjs +5 -18
- package/src/domain/seeded-random.mjs +33 -0
- package/src/domain/syllogise.mjs +10 -7
- package/src/domain/text-stats.mjs +31 -0
- package/src/services/chat.mjs +720 -182
- package/src/services/extract-facts.mjs +155 -0
- package/src/services/import-file.mjs +2 -2
- package/src/services/ledger-viz.mjs +6 -1
- package/src/services/sentences.mjs +26 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +11390 -360
- package/src/tools/graph-load.mjs +7 -1
- package/src/tools/readme-docs.mjs +113 -0
- package/src/tools/schema-docs.mjs +2 -2
- package/corpus/namenet/generate.mjs +0 -309
- package/corpus/wordnet/generate.mjs +0 -332
- package/src/adapters/prose-tokens.mjs +0 -98
- package/src/adapters/wordnet-source.mjs +0 -70
- package/src/domain/corpus-matrix.mjs +0 -87
- package/src/domain/inflect.mjs +0 -67
- package/src/domain/licences.mjs +0 -68
- package/src/domain/markdown-links.mjs +0 -55
- package/src/domain/persona/codegen.mjs +0 -123
- package/src/domain/publish-gate.mjs +0 -41
- package/src/domain/schemaorg/turtle.mjs +0 -25
- package/src/domain/semcor/parse.mjs +0 -87
- package/src/domain/version-stamp.mjs +0 -36
- package/src/domain/wordnet/yaml.mjs +0 -133
package/src/services/chat.mjs
CHANGED
|
@@ -28,11 +28,12 @@ import { loadTemplates, render as renderTemplate } from "../adapters/corpus/temp
|
|
|
28
28
|
import { rankByBiasThenTrust } from "../domain/memory/bias.mjs";
|
|
29
29
|
import { HAS_A_PREDICATE, loadMemory as loadMemoryStore, normFactPredicate, normFactTerm as normFactTermStatic, readFactRows as readStoredFactRows, readRuleRows as readStoredRuleRows } from "../adapters/memory/core.mjs";
|
|
30
30
|
import {
|
|
31
|
-
CAPABILITY_REPORT_CAP, NEG_CAPABLE_OF_PREDICATE,
|
|
32
|
-
isNegatedPredicate, negatedPredicate, positivePredicate,
|
|
31
|
+
CAPABILITY_REPORT_CAP, NEG_CAPABLE_OF_PREDICATE, NEG_SUBCLASS_PREDICATE, capabilityBaseRate,
|
|
32
|
+
capabilityExtension, isNegatedPredicate, negatedPredicate, positivePredicate,
|
|
33
|
+
resolveCapabilityPolarity,
|
|
33
34
|
} from "../domain/memory/capability.mjs";
|
|
34
35
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
35
|
-
import { splitSentences } from "./sentences.mjs";
|
|
36
|
+
import { splitSentences, carriesASentenceBoundary } from "./sentences.mjs";
|
|
36
37
|
import {
|
|
37
38
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
38
39
|
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS, LIST_TRIGGERS,
|
|
@@ -43,6 +44,7 @@ import { setConstructionBanks } from "../domain/interpret/strategies/constructio
|
|
|
43
44
|
import { nlpAdapter } from "../adapters/ask-nlp.mjs";
|
|
44
45
|
import { readConstructionFiles } from "../adapters/corpus/construction-banks.mjs";
|
|
45
46
|
import { fuzzyMatchInSet, fuzzyBound } from "../domain/interpret/fuzzy.mjs";
|
|
47
|
+
import { loadLexicon, lookupNoun } from "../domain/grammar/lexicon.mjs";
|
|
46
48
|
import { pickPhrase } from "../domain/answer-variants.mjs";
|
|
47
49
|
|
|
48
50
|
// Composition: the chat surface supplies the domain parser's default lemma/POS
|
|
@@ -453,6 +455,37 @@ const IMPLICIT_ANAPHORA_COUNT_RE = /^(?:(?:and|so|then|also)\s+)?how many (?:are
|
|
|
453
455
|
* "defines" vs "contains" by the resolved subject's own class. */
|
|
454
456
|
const AMBIGUOUS_HAVE_VERBS = new Set(["have", "has", "holds", "hold"]);
|
|
455
457
|
|
|
458
|
+
/** Words that can trail a bare count without restricting it: the copula/expletive
|
|
459
|
+
* of "are there", the "in total"/"in the graph" locatives, and the "do you know"
|
|
460
|
+
* politeness. Anything left after these is stripped is a real restrictor the
|
|
461
|
+
* header count cannot evaluate. Mirrors ask.mjs's AGG_TAIL_FILLER plus the
|
|
462
|
+
* interrogative scaffolding that only appears on the chat surface. */
|
|
463
|
+
const COUNT_TAIL_FILLER = new Set([
|
|
464
|
+
"are", "is", "were", "was", "there", "of",
|
|
465
|
+
"in", "total", "altogether", "overall",
|
|
466
|
+
"the", "a", "an", "do", "does", "did", "you", "we", "us", "me", "know",
|
|
467
|
+
"exist", "exists", "existing", "present", "here", "now", "currently",
|
|
468
|
+
"graph", "index", "codebase", "repo", "repository", "memory",
|
|
469
|
+
"that", "this", "known", "recorded", "listed", "stored",
|
|
470
|
+
// trailing discourse particles — "…are there then", "…anyway" — never restrict a count
|
|
471
|
+
"then", "so", "uh", "um", "er", "eh", "well", "though", "anyway", "anyhow",
|
|
472
|
+
"again", "really", "actually", "maybe", "perhaps", "just", "simply", "rather", "please",
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
/** A count tail past the kind noun carries a real restrictor — one the header
|
|
476
|
+
* count cannot evaluate — iff a content word survives the filler strip and the
|
|
477
|
+
* tail is not the have-family kept on the bare-count path (AMBIGUOUS_HAVE_VERBS).
|
|
478
|
+
* A topical tail ("about tasks", "with tasks", "related to tasks") names nothing
|
|
479
|
+
* RESTRICTOR_VERB_RE recognises, so without this it was silently discarded to the
|
|
480
|
+
* unqualified total; now it declines to the ask engine's honest miss instead. */
|
|
481
|
+
function countTailIsUnhandledRestrictor(tail) {
|
|
482
|
+
const words = String(tail).toLowerCase().replace(/[^a-z\s]+/g, " ").split(/\s+/).filter(Boolean);
|
|
483
|
+
const content = words.filter((w) => !COUNT_TAIL_FILLER.has(w));
|
|
484
|
+
if (!content.length) return false;
|
|
485
|
+
if (content.some((w) => AMBIGUOUS_HAVE_VERBS.has(w))) return false;
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
456
489
|
/** A "how many <kind> …" tail carries a genuine RESTRICTOR clause — not filler — iff
|
|
457
490
|
* it names a real relation verb (active, from VERB_TO_KIND, or passive-participle,
|
|
458
491
|
* from PASSIVE_PARTICIPLE_TO_KIND — both ask-vocab.mjs's closed vocabulary, the
|
|
@@ -501,7 +534,11 @@ export function answerCount(graph, query) {
|
|
|
501
534
|
if (!m) return null;
|
|
502
535
|
const noun = m[1].toLowerCase();
|
|
503
536
|
const cls = COUNT_NOUNS[noun];
|
|
504
|
-
if (cls
|
|
537
|
+
if (cls) {
|
|
538
|
+
const tail = String(query).slice(m.index + m[0].length);
|
|
539
|
+
if (RESTRICTOR_VERB_RE.test(tail)) return null;
|
|
540
|
+
if (countTailIsUnhandledRestrictor(tail)) return null;
|
|
541
|
+
}
|
|
505
542
|
if (!cls) {
|
|
506
543
|
const kinds = countableKinds(graph);
|
|
507
544
|
// When no code graph is loaded, countableKinds(graph) is genuinely
|
|
@@ -684,6 +721,29 @@ const MEMORY_COUNT_NOUNS = {
|
|
|
684
721
|
};
|
|
685
722
|
const MEMORY_CLASS_LABELS = { Fact: ["fact", "facts"], Utterance: ["utterance", "utterances"] };
|
|
686
723
|
|
|
724
|
+
/** Everything a count question can trail after its counted noun and still mean
|
|
725
|
+
* the plain total: "how many facts do you know", "how many facts are there",
|
|
726
|
+
* "how many facts in total". A closed table — anything outside it restricts
|
|
727
|
+
* the count to something, and this lane says so rather than answering as if
|
|
728
|
+
* it were not there. */
|
|
729
|
+
const MEMORY_COUNT_FILLER_TAIL_RE =
|
|
730
|
+
/^(?:(?:do|d')\s+(?:you|u)\s+(?:know|have|remember)|are\s+there|(?:in\s+)?(?:total|all)|altogether)?[?.!\s]*$/i;
|
|
731
|
+
|
|
732
|
+
/** "how many facts about horses (are there)" — the one restriction this lane
|
|
733
|
+
* reads: facts naming a term on either side of the triple. */
|
|
734
|
+
const MEMORY_COUNT_ABOUT_TAIL_RE =
|
|
735
|
+
/^about\s+(?:the\s+|an?\s+)?([a-z][\w-]*)\s*(?:are\s+there|do\s+(?:you|u)\s+know)?[?.!\s]*$/i;
|
|
736
|
+
|
|
737
|
+
/** Count the stored Facts naming `term` as subject or object — the restriction
|
|
738
|
+
* "how many facts about horses" asks for. */
|
|
739
|
+
async function memoryFactsAboutCount(memoryDir, term) {
|
|
740
|
+
const { loadMemory, readFactRows, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
741
|
+
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
742
|
+
const wanted = normFactTerm(singularOf(term, loadLexicon(), lookupNoun));
|
|
743
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
744
|
+
return rows.filter((r) => normFactTerm(r.subject) === wanted || normFactTerm(r.object) === wanted).length;
|
|
745
|
+
}
|
|
746
|
+
|
|
687
747
|
/** Recognise a memory-store count question and answer it by loading the memory
|
|
688
748
|
* graph, or null (→ answerCount / the ask engine own it). Handles "how many facts",
|
|
689
749
|
* "how many utterances", and the bare "how many do you know" (→ facts). Lazy +
|
|
@@ -693,20 +753,41 @@ async function answerMemoryCount(memoryDir, query) {
|
|
|
693
753
|
if (!memoryDir) return null;
|
|
694
754
|
const q = String(query).toLowerCase();
|
|
695
755
|
let cls = null;
|
|
756
|
+
let tail = "";
|
|
696
757
|
// the bare "how many do you know" (no explicit noun) defaults to remembered facts
|
|
697
758
|
if (/\bhow many(?:\s+(?:things?|facts?))?\s+(?:do|d'?)\s+(?:you|u)\s+know\b/.test(q)) cls = "Fact";
|
|
698
759
|
if (!cls) {
|
|
699
|
-
const m = q.match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b
|
|
700
|
-
if (m)
|
|
760
|
+
const m = q.match(/\b(?:how many|number of|count(?:\s+the)?)\s+([a-z]+)\b(.*)$/);
|
|
761
|
+
if (m) {
|
|
762
|
+
cls = MEMORY_COUNT_NOUNS[m[1]] || null;
|
|
763
|
+
tail = m[2].trim();
|
|
764
|
+
}
|
|
701
765
|
}
|
|
702
766
|
if (!cls) return null;
|
|
767
|
+
const [sing, plur] = MEMORY_CLASS_LABELS[cls];
|
|
768
|
+
const said = (n) => `${n} ${n === 1 ? sing : plur}.`;
|
|
769
|
+
// A restriction this lane can read: count only the facts naming that term.
|
|
770
|
+
const about = cls === "Fact" ? tail.match(MEMORY_COUNT_ABOUT_TAIL_RE) : null;
|
|
771
|
+
if (about) {
|
|
772
|
+
try {
|
|
773
|
+
return `${said(await memoryFactsAboutCount(memoryDir, about[1]))} (about "${about[1]}")`;
|
|
774
|
+
} catch {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// A tail that restricts the question to something this lane cannot read. The
|
|
779
|
+
// total is not the answer to it — it is the answer to a shorter question
|
|
780
|
+
// nobody asked — so name what went unread instead of counting past it.
|
|
781
|
+
if (tail && !MEMORY_COUNT_FILLER_TAIL_RE.test(tail)) {
|
|
782
|
+
return `I can count the ${plur} I hold, but not the "${tail}" part of that question — `
|
|
783
|
+
+ `so I won't answer with the plain total, which would be a count of something you didn't ask for. `
|
|
784
|
+
+ `Ask "how many ${plur} do you know" for the total, or "how many ${plur} about <term>" to narrow it.`;
|
|
785
|
+
}
|
|
703
786
|
let loadMemory;
|
|
704
787
|
try { ({ loadMemory } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
705
788
|
let mem;
|
|
706
789
|
try { mem = await loadMemory(memoryDir); } catch { return null; }
|
|
707
|
-
|
|
708
|
-
const [sing, plur] = MEMORY_CLASS_LABELS[cls];
|
|
709
|
-
return `${n} ${n === 1 ? sing : plur}.`;
|
|
790
|
+
return said((mem.individuals || []).filter((i) => (i.class || "") === cls).length);
|
|
710
791
|
}
|
|
711
792
|
|
|
712
793
|
/** `/stats`: a one-screen overview of the graph — class counts, relationship
|
|
@@ -1786,8 +1867,8 @@ const OWNS_PASSIVE_TEACH_RE = /^(.+?)\s+(?:is|are|was|were)\s+owned\s+by\s+([A-Z
|
|
|
1786
1867
|
* a NAMED relationship between two entities ("ahab is the father of john"),
|
|
1787
1868
|
* grouped here with the other relational/possessive teach shapes above
|
|
1788
1869
|
* (ownership) since it's tried on the SAME ownSrc in teachLane, right after
|
|
1789
|
-
* OWNS_PASSIVE_TEACH_RE
|
|
1790
|
-
*
|
|
1870
|
+
* OWNS_PASSIVE_TEACH_RE — unconditionally ahead of generalVerbTeach's own
|
|
1871
|
+
* call site. The literal "the" + bare role-noun +
|
|
1791
1872
|
* "of" anchor is deliberate: a future composition-rule teach shape ("a
|
|
1792
1873
|
* <rule> is a <relation> of a <relation>") uses an INDEFINITE "a"/"an" in
|
|
1793
1874
|
* the same slot instead, so the two shapes structurally can never collide —
|
|
@@ -2051,9 +2132,32 @@ const GOAL_TEACH_VERBLESS_RE = new RegExp(
|
|
|
2051
2132
|
// preposition), so this one reader answers either.
|
|
2052
2133
|
const ACTION_SIGNATURE_ASK_RE = new RegExp(
|
|
2053
2134
|
`^(?:can|could)\\s+you\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)[?.!]*$`, "i");
|
|
2054
|
-
const PLAN_SOLVE_RE = /^(?:solve\s+it|plan\s+the\s+moves|how\s+do\s+i\s+get(?:\s+from\s+here)?\s+to\s+the\s+goal)[?.!\s]*$/i;
|
|
2135
|
+
const PLAN_SOLVE_RE = /^(?:solve\s+it|solve\s+(?:the\s+)?(?:towers?\s+of\s+hanoi|hanoi|puzzle|game|river\s+crossing|this)|plan\s+the\s+moves|how\s+do\s+i\s+get(?:\s+from\s+here)?\s+to\s+the\s+goal)[?.!\s]*$/i;
|
|
2055
2136
|
const LEGAL_MOVES_RE = /^what\s+moves\s+are\s+legal(?:\s+now)?[?.!\s]*$/i;
|
|
2056
2137
|
const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
2138
|
+
// The imperative voicing of a universal goal ("get all the disks onto peg-c"):
|
|
2139
|
+
// like the verbless frame it names no board verb, so planLaneAnswer reads that
|
|
2140
|
+
// off the taught locative facts. Captures a quantifier, a (possibly plural)
|
|
2141
|
+
// class term, the preposition and the target — the caller singularizes the
|
|
2142
|
+
// term and normalizes "onto"→"on" before the same verbless resolution runs.
|
|
2143
|
+
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");
|
|
2145
|
+
// Plan follow-up questions, answered off the ACTIVE plan state (never invented
|
|
2146
|
+
// when no plan stands). "next move"/"continue" EXECUTE (PLAN_NEXT_RE above); these
|
|
2147
|
+
// three only REPORT.
|
|
2148
|
+
const PLAN_WHAT_NEXT_RE = /^(?:what(?:'s|\s+is)?|whats)\s+the\s+next\s+move[?.!\s]*$/i;
|
|
2149
|
+
const PLAN_MOVE_COUNT_RE = /^how\s+many\s+moves(?:\s+(?:are\s+(?:there|left)|remain(?:ing)?|left|to\s+go|in\s+the\s+plan|total))?[?.!\s]*$/i;
|
|
2150
|
+
const PLAN_WHY_MOVE_RE = /^why\s+(?:that|this|the\s+next|the)\s+move[?.!\s]*$/i;
|
|
2151
|
+
// Board-state read-backs, answered off the CURRENT board (the latest @stepK
|
|
2152
|
+
// snapshot, or the taught board before any step) so a read never contradicts
|
|
2153
|
+
// the plan's own board@stepK line. Clearness is derived, never stored: a piece
|
|
2154
|
+
// is clear iff nothing rests on it on the current board.
|
|
2155
|
+
const IS_CLEAR_RE = /^(?:is|are)\s+([\w-]+)\s+clear[?.!\s]*$/i;
|
|
2156
|
+
const BOARD_REVERSE_LOC_RE = new RegExp(
|
|
2157
|
+
`^(?:what|who)\\s+([a-z']+)\\s+(${PREP_SRC})\\s+(.+?)[?.!\\s]*$`, "i");
|
|
2158
|
+
const BOARD_FORWARD_LOC_RE = new RegExp(
|
|
2159
|
+
`^what\\s+(?:does|do|is)\\s+([\\w-]+)\\s+([a-z]+)(?:\\s+(${PREP_SRC}))?[?.!\\s]*$`, "i");
|
|
2160
|
+
const BOARD_WHERE_RE = /^(?:where\s+is|where's)\s+([\w-]+)(?:\s+now)?[?.!\s]*$/i;
|
|
2057
2161
|
|
|
2058
2162
|
/** "<X> is <adjective>" — the property teach payload (wrapper-REQUIRED): a lazy
|
|
2059
2163
|
* subject and a single bare complement word. Never matches the "is a <noun>"
|
|
@@ -2115,11 +2219,6 @@ function singularizeSurface(word) {
|
|
|
2115
2219
|
return w;
|
|
2116
2220
|
}
|
|
2117
2221
|
|
|
2118
|
-
/** "some Xs are Ys" / "a few Xs are Ys" — the plural class-membership
|
|
2119
|
-
* quantifier shape. Captures the quantifier word itself (group 1) alongside
|
|
2120
|
-
* the plural subject/object (groups 2/3); singularized before storage/lookup. */
|
|
2121
|
-
const SOME_A_FEW_RE = /^(some|a few)\s+([\w-]+)\s+are\s+([\w-]+)$/i;
|
|
2122
|
-
|
|
2123
2222
|
/** "(every|each|all|a|an )?X is/are (a|an )?Y" — the shape the unknown-subject
|
|
2124
2223
|
* fallback recognizes (group 2 = X, group 4 = Y); group 1 (when present)
|
|
2125
2224
|
* names the determiner, so the caller can tell a genuine "every" universal
|
|
@@ -2295,9 +2394,7 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
2295
2394
|
if (classify(subjectRaw, lex)) return null;
|
|
2296
2395
|
const quantifier = /^every$/i.test((det || "").trim()) ? "every" : "";
|
|
2297
2396
|
// Singularize the SUBJECT before storage, but ONLY on a genuinely PLURAL
|
|
2298
|
-
// phrasing ("all men ARE mortal", verb "are")
|
|
2299
|
-
// singularizeSurface() call (above), which is safe unconditionally there
|
|
2300
|
-
// only because that shape's own regex requires "are" by construction. This
|
|
2397
|
+
// phrasing ("all men ARE mortal", verb "are"). This
|
|
2301
2398
|
// shape (UNKNOWN_SUBJECT_RE) also matches singular "is" sentences ("redis
|
|
2302
2399
|
// is a cache"), where singularizing must NEVER run — "redis" naively folds
|
|
2303
2400
|
// to "redi" under the same naive -s-strip. So "all men are mortal" stores
|
|
@@ -2418,7 +2515,7 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon },
|
|
|
2418
2515
|
// unknownSubjectFallback itself already declines for it (its own
|
|
2419
2516
|
// classify(subjectRaw) check reads "men" as the known noun "man") and hands
|
|
2420
2517
|
// off to this mirror fallback instead. A naive suffix-strip
|
|
2421
|
-
// (singularizeSurface
|
|
2518
|
+
// (singularizeSurface, the fallback for
|
|
2422
2519
|
// a genuinely novel REGULAR plural not in the lexicon, e.g. "zorps") can't
|
|
2423
2520
|
// undo an IRREGULAR plural like "men" -> "man" — only the lexicon's own
|
|
2424
2521
|
// noun table (lookupNoun, already resolved by isGroundedTerm/classify to
|
|
@@ -3023,6 +3120,42 @@ function teachSuggestion(payload) {
|
|
|
3023
3120
|
return `every ${subject} is ${article} ${object}`;
|
|
3024
3121
|
}
|
|
3025
3122
|
|
|
3123
|
+
/** "some/a few/several/most/many Xs are Ys" — a claim about SOME members of a
|
|
3124
|
+
* class. Every teach frame in this lane stores a universal: a subClassOf says
|
|
3125
|
+
* each member of the subject class counts as the object, which is what makes
|
|
3126
|
+
* it a premise the syllogiser can chain through. Stored that way, "some men
|
|
3127
|
+
* are fathers" reads back as a proof that any given man is a father, citing
|
|
3128
|
+
* the sentence as its warrant.
|
|
3129
|
+
*
|
|
3130
|
+
* No existential shape exists in this store yet — owl:someValuesFrom is the
|
|
3131
|
+
* adjacent OWL construct, and reaching it means a rule of its own in
|
|
3132
|
+
* syllogise.mjs plus a fact shape that carries the restriction. Until one is
|
|
3133
|
+
* designed, these sentences refuse and name the universal that would work.
|
|
3134
|
+
* "every"/"each"/"all" ARE universals and teach unchanged. */
|
|
3135
|
+
const EXISTENTIAL_CLASS_TEACH_RE = /^(some|a few|several|most|many)\s+([\w-]+)\s+(?:is|are)\s+(?:an?\s+)?([\w-]+)[.!]*$/i;
|
|
3136
|
+
|
|
3137
|
+
/** The lexicon's own lemma for a plural, falling back to the naive suffix
|
|
3138
|
+
* strip — the only source that undoes an irregular plural ("men" -> "man",
|
|
3139
|
+
* which no suffix rule can reach). */
|
|
3140
|
+
const singularOf = (word, lex, lookupNoun) => lookupNoun(lex, word)?.lemma || singularizeSurface(word);
|
|
3141
|
+
|
|
3142
|
+
async function existentialTeachRefusal(payload, lexicon) {
|
|
3143
|
+
const sentence = String(payload || "").trim();
|
|
3144
|
+
const m = sentence.match(EXISTENTIAL_CLASS_TEACH_RE);
|
|
3145
|
+
if (!m) return null;
|
|
3146
|
+
const [, quantifier, subject, object] = m;
|
|
3147
|
+
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
3148
|
+
const lex = lexicon || loadLexicon();
|
|
3149
|
+
const singularSubject = singularOf(subject, lex, lookupNoun);
|
|
3150
|
+
const universal = teachSuggestion(`${singularSubject} is ${singularOf(object, lex, lookupNoun)}`);
|
|
3151
|
+
return {
|
|
3152
|
+
text: `I can't store "${sentence.replace(/[.!]+$/, "")}" — "${quantifier.toLowerCase()}" claims only some of them, `
|
|
3153
|
+
+ "and I store universals, so that isn't a shape I can store yet."
|
|
3154
|
+
+ (universal ? ` If you mean it of every ${singularSubject}, say "${universal}".` : ""),
|
|
3155
|
+
via: "teach-miss", miss: true,
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3026
3159
|
/** The honest decline for a bare habitual teach ("penguins swim") whose
|
|
3027
3160
|
* subject is grounded nowhere — neither the static lexicon nor a prior
|
|
3028
3161
|
* taught fact. Mirrors ungroundedPairHint's "name the gap, hand over a
|
|
@@ -3079,21 +3212,21 @@ const TEACH_PRONOUN_RE = new RegExp(`^(?:every\\s+|each\\s+|all\\s+|some\\s+|a f
|
|
|
3079
3212
|
* when a teach frame offers one. */
|
|
3080
3213
|
const isTeachPronoun = (s) => TEACH_PRONOUNS.includes(String(s || "").trim().toLowerCase());
|
|
3081
3214
|
|
|
3082
|
-
/**
|
|
3083
|
-
*
|
|
3084
|
-
*
|
|
3085
|
-
*
|
|
3086
|
-
*
|
|
3087
|
-
*
|
|
3088
|
-
*
|
|
3089
|
-
*
|
|
3090
|
-
*
|
|
3215
|
+
/** NEGATION of a subClassOf fact: "X is not a Y" (tolerating the same
|
|
3216
|
+
* "kind/type of" infix every other teach shape in this lane already tolerates
|
|
3217
|
+
* — the "is/are" variant, plus the "isn't"/"aren't" contractions).
|
|
3218
|
+
* Deliberately narrow — the SAFEST, most unambiguous negation phrasing only,
|
|
3219
|
+
* matching a scoped 2-token subject (mirrors UNKNOWN_SUBJECT_RE's own subject
|
|
3220
|
+
* width), never a general negation grammar. A match is only a TRIGGER: the
|
|
3221
|
+
* shape also fits a negated PROPERTY claim ("the logger is not deprecated"),
|
|
3222
|
+
* so the stored subject⊑object fact is what decides whether there is a
|
|
3223
|
+
* disagreement to record. */
|
|
3091
3224
|
const RETRACT_NOT_A_RE = /^(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:(?:is|are)\s+not|isn't|aren't)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?([\w-]+)$/i;
|
|
3092
|
-
/** "forget (that) X is a Y" — the
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
3095
|
-
*
|
|
3096
|
-
*
|
|
3225
|
+
/** "forget (that) X is a Y" — the ONLY phrasing that retracts. Never wrapped
|
|
3226
|
+
* by TEACH_RE ("forget" isn't one of its recognized lead verbs —
|
|
3227
|
+
* remember/note/keep in mind/…), so this is matched against the RAW
|
|
3228
|
+
* (unwrapped) sentence, unlike RETRACT_NOT_A_RE above which is tried against
|
|
3229
|
+
* the remember-wrapped surface too. */
|
|
3097
3230
|
const RETRACT_FORGET_RE = /^forget\s+(?:that\s+)?(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:is|are)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?([\w-]+)$/i;
|
|
3098
3231
|
|
|
3099
3232
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null }) {
|
|
@@ -3107,6 +3240,15 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3107
3240
|
const rawInput = applyPreambleFrames(String(query).trim());
|
|
3108
3241
|
const m = rawInput.match(TEACH_RE);
|
|
3109
3242
|
const wrappedInput = m ? m[1].trim() : null;
|
|
3243
|
+
// Refuse an existential BEFORE any frame below can read it as a universal:
|
|
3244
|
+
// every one of them stores "some men are fathers" as a premise meaning every
|
|
3245
|
+
// man, whether it keeps the quantifier as an attribute the reasoner doesn't
|
|
3246
|
+
// consult (the subclass frame) or bakes the word into the subject itself
|
|
3247
|
+
// ("most men is a kind of fathers", the unknown-subject frame).
|
|
3248
|
+
if (memoryDir && !QUESTION_LEAD_RE.test(wrappedInput ?? rawInput)) {
|
|
3249
|
+
const refusal = await existentialTeachRefusal(wrappedInput ?? rawInput, lexicon);
|
|
3250
|
+
if (refusal) return refusal;
|
|
3251
|
+
}
|
|
3110
3252
|
// "your X is a/an Y" — a plain casual synonym for "a/an X is a
|
|
3111
3253
|
// Y": no special second-person semantics, so rewrite it to the ordinary
|
|
3112
3254
|
// indefinite-article determiner UP FRONT, before any downstream regex/ACE
|
|
@@ -3265,31 +3407,13 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3265
3407
|
};
|
|
3266
3408
|
}
|
|
3267
3409
|
|
|
3268
|
-
//
|
|
3269
|
-
//
|
|
3270
|
-
//
|
|
3271
|
-
//
|
|
3272
|
-
//
|
|
3273
|
-
//
|
|
3274
|
-
//
|
|
3275
|
-
// TRIGGER, never itself the authority: RETRACT_NOT_A_RE/RETRACT_FORGET_RE
|
|
3276
|
-
// only recognize the SHAPE of a negation/retraction sentence — they say
|
|
3277
|
-
// nothing about whether subject⊑object was ever actually taught.
|
|
3278
|
-
// retractSubClassOf is asked for real and is the only thing that decides:
|
|
3279
|
-
// - found:true → a real stored (or entailed) fact existed and was
|
|
3280
|
-
// retracted (with its dependency-directed cascade) — confirmed here.
|
|
3281
|
-
// - found:false → subject⊑object was never a stored fact. This is left
|
|
3282
|
-
// to FALL THROUGH to the rest of teachLane's ordinary cascade below,
|
|
3283
|
-
// deliberately NOT answered with a bespoke "nothing to forget" message
|
|
3284
|
-
// — RETRACT_NOT_A_RE's shape also incidentally matches a NEGATED
|
|
3285
|
-
// PROPERTY claim ("the logger is not deprecated" — never subClassOf-
|
|
3286
|
-
// shaped at all, a pinned "genuine ceiling" case elsewhere in this
|
|
3287
|
-
// codebase, test/chatflow-tier5.test.mjs, that must keep its own
|
|
3288
|
-
// "I couldn't store that —" decline verbatim), so a bare "nothing
|
|
3289
|
-
// found" here must never claim a specific, possibly-wrong reason —
|
|
3290
|
-
// falling through preserves whatever honest response that OTHER shape
|
|
3291
|
-
// already gets, byte-identical, while still fully closing the real gap
|
|
3292
|
-
// (an already-taught fact's retraction, which now always succeeds).
|
|
3410
|
+
// NEGATION ("X is not a Y") and RETRACTION ("forget that X is a Y"). Two
|
|
3411
|
+
// sentences, two intents, and the split is the point: a negative is a source
|
|
3412
|
+
// DISAGREEING, never an instruction to destroy. Each branch documents itself
|
|
3413
|
+
// below; both are tried here, right after the pronoun guard, so a pronoun
|
|
3414
|
+
// subject ("it is not an animal") still falls to that guard's own decline
|
|
3415
|
+
// first (TEACH_PRONOUN_RE matches ANY verb after the pronoun, including
|
|
3416
|
+
// "is not"), never reaching this block.
|
|
3293
3417
|
const retractSrc = (wrapped ?? raw).replace(/[.!?]+\s*$/, "");
|
|
3294
3418
|
const retractSrcMidQuestion = memoryDir && !QUESTION_LEAD_RE.test(retractSrc)
|
|
3295
3419
|
? await hasMidSentenceInterrogative(retractSrc) : false;
|
|
@@ -3302,22 +3426,76 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3302
3426
|
if (retractMatch) {
|
|
3303
3427
|
const retractSubject = retractMatch[1].trim();
|
|
3304
3428
|
const retractObject = retractMatch[2].trim();
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3429
|
+
|
|
3430
|
+
// A BARE NEGATIVE IS A CLAIM, NOT AN INSTRUCTION TO DELETE. "john is not a
|
|
3431
|
+
// man" disagrees with a stored fact; it does not ask for it to be
|
|
3432
|
+
// destroyed, and destroying it loses the very disagreement the user came
|
|
3433
|
+
// to record. Both polarities are stored under their own predicate
|
|
3434
|
+
// (memory/capability.mjs: a fact id hashes (subject, predicate, object), so
|
|
3435
|
+
// sharing one predicate would merge them and union their statedBy edges),
|
|
3436
|
+
// and the ask ladder reports the disagreement rather than picking a side.
|
|
3437
|
+
// Only the explicit "forget that X is a Y" verb retracts — see below.
|
|
3438
|
+
//
|
|
3439
|
+
// GATED ON THE POSITIVE EXISTING, and that gate is load-bearing:
|
|
3440
|
+
// RETRACT_NOT_A_RE's shape also incidentally matches a negated PROPERTY
|
|
3441
|
+
// claim ("the logger is not deprecated"), which is never subClassOf-shaped
|
|
3442
|
+
// at all and keeps its own decline verbatim. With no stored subject⊑object
|
|
3443
|
+
// to disagree with, there is nothing here to record, so the sentence falls
|
|
3444
|
+
// through to the ordinary cascade exactly as it always has.
|
|
3445
|
+
if (retractNotMatch) {
|
|
3446
|
+
const { SUBCLASS_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
3447
|
+
const { loadMemory: loadMemForNeg, normFactTerm: normTermForNeg, readFactRows: readRowsForNeg } = await import("../adapters/memory/core.mjs");
|
|
3448
|
+
const negSubject = normTermForNeg(retractSubject);
|
|
3449
|
+
const negObject = normTermForNeg(retractObject);
|
|
3450
|
+
const priorRows = readRowsForNeg(await loadMemForNeg(memoryDir));
|
|
3451
|
+
const positive = priorRows.find((r) => r.subject === negSubject && r.predicate === SUBCLASS_PREDICATE && r.object === negObject);
|
|
3452
|
+
if (positive) {
|
|
3453
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3454
|
+
subject: retractSubject, predicate: NEG_SUBCLASS_PREDICATE, object: retractObject,
|
|
3455
|
+
});
|
|
3456
|
+
if (stored) {
|
|
3457
|
+
return {
|
|
3458
|
+
...stored,
|
|
3459
|
+
text: `${stored.text} — you told me earlier that ${negSubject} is a kind of ${negObject}, so both are now stored `
|
|
3460
|
+
+ `and I'll report the disagreement rather than pick one. `
|
|
3461
|
+
+ `To drop the earlier fact instead, say "forget that ${negSubject} is ${indefiniteArticleFor(negObject)} ${negObject}".`,
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
// nothing stored to disagree with — fall through (see the gate above).
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// RETRACTION — "forget that X is a Y": wires the data-layer retraction
|
|
3469
|
+
// primitive (retractSubClassOf, src/domain/syllogise.mjs) up to chat-level
|
|
3470
|
+
// phrasing.
|
|
3471
|
+
//
|
|
3472
|
+
// TRIGGER, never itself the authority: RETRACT_FORGET_RE only recognizes
|
|
3473
|
+
// the SHAPE of a retraction sentence — it says nothing about whether
|
|
3474
|
+
// subject⊑object was ever actually taught. retractSubClassOf is asked for
|
|
3475
|
+
// real and is the only thing that decides:
|
|
3476
|
+
// - found:true → a real stored (or entailed) fact existed and was
|
|
3477
|
+
// retracted (with its dependency-directed cascade) — confirmed here.
|
|
3478
|
+
// - found:false → subject⊑object was never a stored fact, and this falls
|
|
3479
|
+
// through to the rest of teachLane's ordinary cascade below rather than
|
|
3480
|
+
// claiming a specific, possibly-wrong reason.
|
|
3481
|
+
if (retractForgetMatch) {
|
|
3482
|
+
const { retractSubClassOf } = await import("../domain/syllogise.mjs");
|
|
3483
|
+
const { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts } = await import("../adapters/memory/core.mjs");
|
|
3484
|
+
const result = await retractSubClassOf(memoryDir, retractSubject, retractObject, {
|
|
3485
|
+
store: { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts },
|
|
3486
|
+
});
|
|
3487
|
+
if (result.found) {
|
|
3488
|
+
const extra = result.count - 1; // beyond the target fact itself
|
|
3489
|
+
return {
|
|
3490
|
+
text: `noted — forgotten: "${retractSubject} is a kind of ${retractObject}" is no longer stored`
|
|
3491
|
+
+ (extra > 0 ? ` (${extra} entailed fact${extra === 1 ? "" : "s"} that depended on it went too)` : "")
|
|
3492
|
+
+ (result.truncated ? " — this cascade may not be complete (a lot depended on it); ask again if something still looks stale" : "")
|
|
3493
|
+
+ ".",
|
|
3494
|
+
via: "retract", miss: false,
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
// found:false — fall through to the rest of the cascade (see docblock above).
|
|
3319
3498
|
}
|
|
3320
|
-
// found:false — fall through to the rest of the cascade (see docblock above).
|
|
3321
3499
|
}
|
|
3322
3500
|
|
|
3323
3501
|
// OWNERSHIP — "<Name> owns/maintains <X>", bare or remember-wrapped. The bare
|
|
@@ -3729,59 +3907,6 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3729
3907
|
if (stored) return stored;
|
|
3730
3908
|
}
|
|
3731
3909
|
|
|
3732
|
-
// "some Xs are Ys" / "a few Xs are Ys" — the plural class-
|
|
3733
|
-
// membership quantifier shape. ACE has no quantifier-phrase pattern at all
|
|
3734
|
-
// (parseAce never even attempts a fit), so this is ALWAYS a direct write,
|
|
3735
|
-
// never routed through assertTurn below. Wrapper-optional, like the
|
|
3736
|
-
// "every X is a Y" baseline — a plural "some/a few" claim reads as an
|
|
3737
|
-
// ordinary declarative teach the same way "every" always has. The OBJECT
|
|
3738
|
-
// still has to be a known lexicon noun (the same "subject gets the free
|
|
3739
|
-
// pass, object doesn't" discipline as unknownSubjectFallback below) — an
|
|
3740
|
-
// unknown object falls through to the generic honest-miss cascade at the
|
|
3741
|
-
// bottom of this function, same as every other unstorable teach.
|
|
3742
|
-
const someSrc = wrapped ?? raw.replace(/[.!?]+\s*$/, "");
|
|
3743
|
-
// Additive alongside the existing anchored QUESTION_LEAD_RE check — same
|
|
3744
|
-
// discipline as ownSrcMidQuestion above (hasMidSentenceInterrogative's own
|
|
3745
|
-
// docblock, near QUESTION_LEAD_RE, has the full reasoning).
|
|
3746
|
-
const someSrcMidQuestion = memoryDir && !QUESTION_LEAD_RE.test(someSrc) ? await hasMidSentenceInterrogative(someSrc) : false;
|
|
3747
|
-
const someMatch = memoryDir && !QUESTION_LEAD_RE.test(someSrc) && !someSrcMidQuestion ? someSrc.match(SOME_A_FEW_RE) : null;
|
|
3748
|
-
if (someMatch) {
|
|
3749
|
-
const quantifier = someMatch[1].toLowerCase();
|
|
3750
|
-
const subject = singularizeSurface(someMatch[2]);
|
|
3751
|
-
const object = singularizeSurface(someMatch[3]);
|
|
3752
|
-
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
3753
|
-
const lex = lexicon || loadLexicon();
|
|
3754
|
-
if (lookupNoun(lex, object)) {
|
|
3755
|
-
const stored = await teachFact(memoryDir, sessionId, {
|
|
3756
|
-
subject, predicate: SUBCLASS_PREDICATE, object, quantifier,
|
|
3757
|
-
});
|
|
3758
|
-
if (stored) return stored;
|
|
3759
|
-
} else {
|
|
3760
|
-
// "remember that some functions are risky" — Y ("risky") is not a
|
|
3761
|
-
// lexicon NOUN, so the subclass path just above correctly declines it
|
|
3762
|
-
// (SOME_A_FEW_RE is subclass-only). Without this guard, the sentence
|
|
3763
|
-
// would fall through to unknownSubjectFallback/TEACH_PROPERTY_RE below,
|
|
3764
|
-
// which DO tolerate a multi-word subject with NO vocabulary check on
|
|
3765
|
-
// the complement at all — silently mis-teaching the LITERAL 2-word
|
|
3766
|
-
// string "some functions" as if it were one proper-noun subject
|
|
3767
|
-
// ("noted — remembered: some functions is risky", the quantifier word
|
|
3768
|
-
// baked wrongly into the subject and a subject/verb agreement error to
|
|
3769
|
-
// boot), a fact "how many functions are risky" could never sensibly
|
|
3770
|
-
// read back either (HOW_MANY_ARE_RE's own reader only ever looks for
|
|
3771
|
-
// the SUBCLASS_PREDICATE shape this path would have stored, not this
|
|
3772
|
-
// one). A quantified PROPERTY claim isn't a supported shape yet (only
|
|
3773
|
-
// a quantified SUBCLASS claim is) — decline honestly here instead of
|
|
3774
|
-
// silently mis-teaching, rather than let a later, less-specific frame
|
|
3775
|
-
// guess a wrong split.
|
|
3776
|
-
return {
|
|
3777
|
-
text: `I can only remember a quantified fact as "${quantifier} ${someMatch[2]} are <a kind of thing>" (like "${quantifier} bugs are issues") — `
|
|
3778
|
-
+ `a quantified claim about a PROPERTY ("${quantifier} ${someMatch[2]} are ${object}") isn't a shape I can store yet. `
|
|
3779
|
-
+ `I can remember "${someMatch[2]} are ${object}" for one specific ${subject}, though — try naming it directly.`,
|
|
3780
|
-
via: "teach-miss", miss: true,
|
|
3781
|
-
};
|
|
3782
|
-
}
|
|
3783
|
-
}
|
|
3784
|
-
|
|
3785
3910
|
// GENERAL VERB-TO-PREDICATE TEACH — "remember <Subject> <verb>
|
|
3786
3911
|
// <Object>" where <verb> is neither is/are (handled below via the ACE/
|
|
3787
3912
|
// unknown-subject/property paths) nor owns/maintains (handled above).
|
|
@@ -4099,6 +4224,18 @@ const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB
|
|
|
4099
4224
|
// tolerance for the bare "whats" contraction spelling, just below.
|
|
4100
4225
|
const MODULE_PURPOSE_RE = /^what(?:'s|s|\s+is)\s+(.+?)\s+(?:for|about)\??$/i;
|
|
4101
4226
|
|
|
4227
|
+
/** A module PATH as a reader types it — "src/core/store.mjs", "app/lib/b.mjs",
|
|
4228
|
+
* or a bare "store.mjs". Requires a slash or a source-file extension, which is
|
|
4229
|
+
* what makes the two identity phrasings below safe to claim: no vocabulary
|
|
4230
|
+
* term can match this shape, so "what is a dog" is untouched. The lane's
|
|
4231
|
+
* exact-unique resolveEntity gate is still the authority — this only decides
|
|
4232
|
+
* what is worth ASKING it about. */
|
|
4233
|
+
const MODULE_PATH_RE = /^(?:[\w.@~-]+\/)+[\w.@~-]+$|^[\w.@~-]+\.(?:mjs|cjs|js|jsx|ts|tsx|py|java|rb|go|rs|php|cs|kt|swift)$/i;
|
|
4234
|
+
/** "what is src/core/store.mjs" — the identity phrasing of the same question
|
|
4235
|
+
* "what does X do" already answers. Kept distinct from MODULE_PURPOSE_RE
|
|
4236
|
+
* ("what is X for"), whose trailing "for"/"about" is what anchors it. */
|
|
4237
|
+
const MODULE_IDENTITY_RE = /^what(?:'s|s|\s+is)\s+(.+?)$/i;
|
|
4238
|
+
|
|
4102
4239
|
/** A leading politeness/formal-ESL wrapper this lane's own anchored regexes
|
|
4103
4240
|
* otherwise miss entirely: "please explain what does X do"
|
|
4104
4241
|
* starts with neither "what"/"whats" (MODULE_ORIENT_RE/MODULE_PURPOSE_RE's own
|
|
@@ -4141,8 +4278,16 @@ async function moduleOrientLane(query, { graph }) {
|
|
|
4141
4278
|
// regex only adds the "explain [to me]" wrapper on top).
|
|
4142
4279
|
q = stripFillerWords(applyPreambleFrames(correctMisspellings(q))).replace(MODULE_ORIENT_POLITENESS_RE, "");
|
|
4143
4280
|
const m = q.match(MODULE_ORIENT_RE) || q.match(MODULE_PURPOSE_RE) || q.match(MODULE_ORIENT_SVO_RE);
|
|
4144
|
-
|
|
4145
|
-
|
|
4281
|
+
// "what does src/core/store.mjs do" already reached the overview; the bare
|
|
4282
|
+
// path and "what is <path>" did not, so the same module answered one
|
|
4283
|
+
// phrasing and walled two. Both are claimed here rather than in ask.mjs,
|
|
4284
|
+
// whose Module fallback is absent BY DESIGN — adding it there replaces this
|
|
4285
|
+
// rich, module-grain overview with a thin one. Gated on the term looking
|
|
4286
|
+
// like a path, so this widens the lane by exactly the shape that was
|
|
4287
|
+
// missing and can never claim a vocabulary question.
|
|
4288
|
+
const identity = m ? null : (q.match(MODULE_IDENTITY_RE)?.[1]?.trim() ?? q);
|
|
4289
|
+
const term = m ? m[1].trim() : (identity && MODULE_PATH_RE.test(identity) ? identity : null);
|
|
4290
|
+
if (!term) return null;
|
|
4146
4291
|
if (/^(?:it|this|that|they|them)$/i.test(term)) return null;
|
|
4147
4292
|
const ent = await resolveEntity(graph, term);
|
|
4148
4293
|
if (!ent) return null;
|
|
@@ -4693,6 +4838,7 @@ async function recallFromBlocks(memoryDir, query, graph) {
|
|
|
4693
4838
|
* predicate renders verbatim rather than being guessed around. */
|
|
4694
4839
|
const FACT_PREDICATE_PHRASES = {
|
|
4695
4840
|
"rdfs:subClassOf": "is a kind of",
|
|
4841
|
+
"mgxneg:subClassOf": "is not a kind of",
|
|
4696
4842
|
"rdf:type": "is a",
|
|
4697
4843
|
"owl:disjointWith": "is not a",
|
|
4698
4844
|
"mgx:partOf": "is part of",
|
|
@@ -4862,6 +5008,40 @@ function renderFactLine(f) {
|
|
|
4862
5008
|
return `i learned: ${factPhrase(f)}${cite}`;
|
|
4863
5009
|
}
|
|
4864
5010
|
|
|
5011
|
+
/** "a"/"an" for a term, through the SAME grammar-rules.toml "article" rule and
|
|
5012
|
+
* finish.mjs's beginsWithVowelSound every other agreement site in this file
|
|
5013
|
+
* uses — never a hardcoded "a", which is ungrammatical for a vowel-initial
|
|
5014
|
+
* term ("forget that task is a animal"). */
|
|
5015
|
+
function indefiniteArticleFor(term) {
|
|
5016
|
+
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
5017
|
+
return articleRule && beginsWithVowelSound(String(term || ""), articleRule) ? "an" : "a";
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
/** The verdict on an is-a question, given the best stored fact of each
|
|
5021
|
+
* polarity. Null when neither is stored, so a caller's own honest miss stands
|
|
5022
|
+
* — an absent positive is never a "no".
|
|
5023
|
+
*
|
|
5024
|
+
* BOTH POLARITIES STORED names both sources and picks NOTHING, which is the
|
|
5025
|
+
* "both" verdict memory/capability.mjs already defines for the capability
|
|
5026
|
+
* family. Preferring either one would rank a tie-break the reader can't see
|
|
5027
|
+
* above what they actually said; recency in particular looks like a
|
|
5028
|
+
* correction and is just as often a second speaker.
|
|
5029
|
+
*
|
|
5030
|
+
* Shared by the two is-a readers (the memory-facts lane and the full ladder),
|
|
5031
|
+
* so a disagreement reads identically whichever one answers. */
|
|
5032
|
+
function isaPolarityReply(hit, negHit) {
|
|
5033
|
+
if (hit && negHit) {
|
|
5034
|
+
return {
|
|
5035
|
+
text: `you've told me both, and I won't pick between them — ${renderFactLine(hit)}; ${renderFactLine(negHit)}. `
|
|
5036
|
+
+ `To settle it, say "forget that ${hit.subject} is ${indefiniteArticleFor(hit.object)} ${hit.object}".`,
|
|
5037
|
+
replace: true,
|
|
5038
|
+
};
|
|
5039
|
+
}
|
|
5040
|
+
if (negHit) return { text: `no — ${renderFactLine(negHit)}`, replace: true };
|
|
5041
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
5042
|
+
return null;
|
|
5043
|
+
}
|
|
5044
|
+
|
|
4865
5045
|
/** PROOF-CHAIN RECEIPT — "renderable as a chain of thought in words": render
|
|
4866
5046
|
* an ordered list of
|
|
4867
5047
|
* premise Fact rows as one continuous argument — "cache is a kind of store;
|
|
@@ -4927,11 +5107,71 @@ async function factRows(memoryDir, cache = null) {
|
|
|
4927
5107
|
|
|
4928
5108
|
/** Spelling variants a question term is matched under (normFactTerm + a naive
|
|
4929
5109
|
* singular): "caches"/"a cache"/"/c/en/cache" all reach the stored "cache". */
|
|
5110
|
+
/** The lexicon's declared plural → lemma map ("men"→"man", "people"→"person"),
|
|
5111
|
+
* loaded once. It carries ONLY the irregulars — a regular plural is recovered
|
|
5112
|
+
* by the -s/-es fold below, so declaring one would be redundant — which is
|
|
5113
|
+
* exactly the set that fold cannot reach. Failure-tolerated: a broken lexicon
|
|
5114
|
+
* degrades to the fold alone, never a crash. */
|
|
5115
|
+
let declaredNounPlurals = null;
|
|
5116
|
+
function irregularSingularOf(word) {
|
|
5117
|
+
if (!declaredNounPlurals) {
|
|
5118
|
+
try { declaredNounPlurals = loadLexicon().nounPlurals; } catch { declaredNounPlurals = new Map(); }
|
|
5119
|
+
}
|
|
5120
|
+
return declaredNounPlurals.get(word) ?? null;
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
/** A subject as it should be SPOKEN BACK in an offered teach sentence: the
|
|
5124
|
+
* lexicon's own lemma for a single known noun, else the reader's words
|
|
5125
|
+
* untouched.
|
|
5126
|
+
*
|
|
5127
|
+
* Without it a plural subject was echoed raw into a singular frame —
|
|
5128
|
+
* "remember that women is mortal" — offering a sentence that is both
|
|
5129
|
+
* ungrammatical and not the shape the teach path stores. lookupNoun is the
|
|
5130
|
+
* lemmatizer the teach path already uses, and it is the whole plural detector:
|
|
5131
|
+
* it folds "women"→woman and "dogs"→dog while leaving "bus" alone, which no
|
|
5132
|
+
* -s rule written here could do. A multi-word or unknown subject is left
|
|
5133
|
+
* exactly as typed — "every zibble is mortal" already reads correctly, and
|
|
5134
|
+
* guessing at a phrase's head would be worse than echoing it. */
|
|
5135
|
+
function teachableSubjectOf(subject) {
|
|
5136
|
+
const raw = String(subject || "").trim().toLowerCase();
|
|
5137
|
+
if (!raw || /\s/.test(raw)) return raw;
|
|
5138
|
+
try {
|
|
5139
|
+
return lookupNoun(loadLexicon(), raw)?.lemma || raw;
|
|
5140
|
+
} catch {
|
|
5141
|
+
return raw;
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
/** A leading universal quantifier, which is scaffolding rather than part of a
|
|
5146
|
+
* name. The teach frames strip exactly these before storing (UNKNOWN_SUBJECT_RE
|
|
5147
|
+
* above carries the same set), so no fact is ever stored under a subject that
|
|
5148
|
+
* begins with one — which is what makes stripping it here a lookup fix and not
|
|
5149
|
+
* a guess. "a"/"an" are deliberately absent: the readers' own regexes already
|
|
5150
|
+
* take the article. */
|
|
5151
|
+
const QUANTIFIER_LEAD_RE = /^(?:every|each|all|any)\s+/i;
|
|
5152
|
+
|
|
4930
5153
|
function factTermVariants(normFactTerm, term) {
|
|
4931
5154
|
const t = normFactTerm(term);
|
|
4932
|
-
const v = new Set(
|
|
4933
|
-
|
|
4934
|
-
|
|
5155
|
+
const v = new Set();
|
|
5156
|
+
// The ask frames glue a quantifier onto the subject and looked up "every
|
|
5157
|
+
// man", a name nothing is stored under, while "is a man mortal" answered.
|
|
5158
|
+
// Both spellings fold through the same plural rules below, so "are all men
|
|
5159
|
+
// mortal" reaches "man" the same way "are men mortal" does.
|
|
5160
|
+
const bases = new Set([t]);
|
|
5161
|
+
const unquantified = t.replace(QUANTIFIER_LEAD_RE, "").trim();
|
|
5162
|
+
if (unquantified && unquantified !== t) bases.add(unquantified);
|
|
5163
|
+
for (const base of bases) {
|
|
5164
|
+
v.add(base);
|
|
5165
|
+
if (base.endsWith("es")) v.add(base.slice(0, -2));
|
|
5166
|
+
if (base.endsWith("s")) v.add(base.slice(0, -1));
|
|
5167
|
+
// An IRREGULAR plural is invisible to the fold above: "men" keeps every
|
|
5168
|
+
// letter of "man" in a different order, so a reader asking "do men die"
|
|
5169
|
+
// looked up a subject no fact is stored under while "does a man die"
|
|
5170
|
+
// answered. The teach path stores the singular, so the ask path has to be
|
|
5171
|
+
// able to reach it.
|
|
5172
|
+
const irregular = irregularSingularOf(base);
|
|
5173
|
+
if (irregular) v.add(normFactTerm(irregular));
|
|
5174
|
+
}
|
|
4935
5175
|
return v;
|
|
4936
5176
|
}
|
|
4937
5177
|
|
|
@@ -5128,8 +5368,8 @@ function matchGenitiveWhoAsk(q) {
|
|
|
5128
5368
|
* reachable from the named start entity through a taught `recursive` Rule,
|
|
5129
5369
|
* not a single yes/no. `m[1]` = the rule's PLURAL name ("descendants",
|
|
5130
5370
|
* singularized via singularizeSurface before the findRuleByName lookup —
|
|
5131
|
-
* the same naive plural fold
|
|
5132
|
-
*
|
|
5371
|
+
* the same naive plural fold the teach-side surfaces use elsewhere in this
|
|
5372
|
+
* file), `m[2]` = the start entity ("ahab"). Dispatch
|
|
5133
5373
|
* lives in factReadBack's own (a0.5) block, below — findRuleByName +
|
|
5134
5374
|
* findReachableSet (src/domain/planning.mjs), never a yes/no answer. */
|
|
5135
5375
|
const RECURSIVE_LIST_ASK_RE = /^list\s+(?:the\s+|all\s+)?([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
@@ -5757,13 +5997,23 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5757
5997
|
// bare catch-all: on the FIRST turn of a graph-less session, dispatchTool's
|
|
5758
5998
|
// loadGraph() throws its own documented empty-graph ToolError (a pre-existing,
|
|
5759
5999
|
// by-design bootstrap behavior — self-corrects from turn 2 on), which leaves
|
|
5760
|
-
// `envelope` null for the rest of the turn, arming this
|
|
5761
|
-
// fallback. Without this guard it greedily swallows the
|
|
5762
|
-
// tail as a literal meta-term to define (mirroring
|
|
5763
|
-
// ARTICLE_RELATION_CONTINUATIONS guard against the
|
|
5764
|
-
// returning early and never letting (b5) below —
|
|
5765
|
-
// exact shape via WHAT_INHERITS_RE, envelope or
|
|
5766
|
-
|
|
6000
|
+
// `envelope` null for the rest of the turn, arming this
|
|
6001
|
+
// no-parse-to-defer-to fallback. Without this guard it greedily swallows the
|
|
6002
|
+
// WHOLE "kind of animal" tail as a literal meta-term to define (mirroring
|
|
6003
|
+
// grammar.mjs T5's OWN ARTICLE_RELATION_CONTINUATIONS guard against the
|
|
6004
|
+
// identical over-capture), returning early and never letting (b5) below —
|
|
6005
|
+
// which already handles this exact shape via WHAT_INHERITS_RE, envelope or
|
|
6006
|
+
// no envelope — get a chance.
|
|
6007
|
+
//
|
|
6008
|
+
// A parse that MISSED is not a parse to defer to. "what are dogs" is claimed
|
|
6009
|
+
// by the composite lane, which declines it ({node:"miss"}, "'dogs' isn't a
|
|
6010
|
+
// listable kind") and by existing merely blocked the vocabulary reader that
|
|
6011
|
+
// answers the singular. The plural is the whole difference: "what are dog"
|
|
6012
|
+
// always worked. A SUCCESSFUL parse still wins here exactly as before — the
|
|
6013
|
+
// over-capture this guard exists to stop parses fine, so it never reaches
|
|
6014
|
+
// this branch.
|
|
6015
|
+
const parsedOwnsIt = envelope?.parsed && envelope.parsed.node !== "miss";
|
|
6016
|
+
if (!metaTerm && miss && !parsedOwnsIt && !WHAT_INHERITS_RE.test(q)) {
|
|
5767
6017
|
const m = q.match(BARE_WHATIS_RE)
|
|
5768
6018
|
|| q.match(/^what\s+(?:does|do)\s+(.+?)\s+means?[?.!\s]*$/i);
|
|
5769
6019
|
// Strip a curated trailing scope clause ("… in this
|
|
@@ -5899,11 +6149,15 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5899
6149
|
if (isa) {
|
|
5900
6150
|
const subj = factTermVariants(normFactTerm, isa[1]);
|
|
5901
6151
|
const obj = factTermVariants(normFactTerm, isa[2]);
|
|
5902
|
-
const
|
|
5903
|
-
|
|
6152
|
+
const isaRows = await memoryFacts(memoryDir);
|
|
6153
|
+
const onTerms = (f) => subj.has(f.subject) && obj.has(f.object);
|
|
6154
|
+
// A remembered NEGATIVE is read on the same terms as the positive — it
|
|
6155
|
+
// carries its own predicate and so never reaches ISA_PREDICATES.
|
|
6156
|
+
const reply = isaPolarityReply(
|
|
6157
|
+
isaRows.find((f) => ISA_PREDICATES.has(f.predicate) && onTerms(f)),
|
|
6158
|
+
isaRows.find((f) => f.predicate === NEG_SUBCLASS_PREDICATE && onTerms(f)),
|
|
5904
6159
|
);
|
|
5905
|
-
|
|
5906
|
-
return null; // no remembered fact — the honest miss stands (never a guessed "no")
|
|
6160
|
+
return reply; // no remembered fact — null, so the honest miss stands (never a guessed "no")
|
|
5907
6161
|
}
|
|
5908
6162
|
|
|
5909
6163
|
// (b2) "can a dog bark" — the polarity of a capability, resolved through the
|
|
@@ -6298,6 +6552,13 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
6298
6552
|
* vocabulary-term lookup for the literal term "in X". */
|
|
6299
6553
|
const WHAT_ELSE_IS_RE = /^what\s+else\s+(?:is|are)\s+(?!in\b|inside\b)(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
6300
6554
|
const WHAT_ELSE_ABOUT_RE = /^what\s+else\s+(?:do\s+you\s+know\s+)?about\s+(?:an?\s+|the\s+)?(.+?)[?.!\s]*$/i;
|
|
6555
|
+
/** The same question with its subject left implicit — "what else", "anything
|
|
6556
|
+
* else", "what else do you know". A reader who has just been told about dogs
|
|
6557
|
+
* and asks "what else" means "what else about dogs"; the subject is carried by
|
|
6558
|
+
* the conversation, exactly as the pronoun in "can it bark" is. So the term
|
|
6559
|
+
* comes from the standing referent (vocabAntecedentFrom) and this shape is a
|
|
6560
|
+
* no-op when nothing is standing. */
|
|
6561
|
+
const WHAT_ELSE_BARE_RE = /^(?:(?:and|so|but|ok|okay|now|then)\s+)*(?:what\s+else(?:\s+do\s+you\s+know)?|anything\s+else(?:\s+you\s+know)?|got\s+anything\s+else)[?.!\s]*$/i;
|
|
6301
6562
|
|
|
6302
6563
|
/** "what else is X" — surface remembered facts about X BEYOND the primary
|
|
6303
6564
|
* curated (corpus/seon) prose definition, which is itself never a Facts row
|
|
@@ -6321,9 +6582,18 @@ async function whatElseAnswer(memoryDir, query, last) {
|
|
|
6321
6582
|
if (!memoryDir) return null;
|
|
6322
6583
|
const q = String(query).trim();
|
|
6323
6584
|
const m = q.match(WHAT_ELSE_IS_RE) || q.match(WHAT_ELSE_ABOUT_RE);
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6585
|
+
// A bare "what else" takes its subject from the standing referent — the same
|
|
6586
|
+
// last-grounded-answer binding "can it bark" uses.
|
|
6587
|
+
const bare = !m && WHAT_ELSE_BARE_RE.test(q);
|
|
6588
|
+
const term = (m ? m[1] : (bare ? vocabAntecedentFrom(last) : null) || "").trim();
|
|
6589
|
+
// Asked cold, it names what it cannot resolve rather than introducing the
|
|
6590
|
+
// tool — the same courtesy a cold pronoun already gets. An identity blurb
|
|
6591
|
+
// answers a question nobody asked.
|
|
6592
|
+
if (!term) {
|
|
6593
|
+
return bare
|
|
6594
|
+
? { text: "Nothing to add yet — there's no subject standing. Ask me about something first, then \"what else\".", replace: true, miss: true }
|
|
6595
|
+
: null;
|
|
6596
|
+
}
|
|
6327
6597
|
let normFactTerm;
|
|
6328
6598
|
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
6329
6599
|
const variants = factTermVariants(normFactTerm, term);
|
|
@@ -6510,7 +6780,7 @@ const IS_ADJECTIVE_YESNO_PRONOUN_SUBJECT_RE = /^(?:you|i|they|he|she|we)\b/i;
|
|
|
6510
6780
|
* reaches TEACH_PROPERTY_RE via BARE_DECLARATIVE_RE's single-token-subject
|
|
6511
6781
|
* restriction and would fail here). */
|
|
6512
6782
|
const unknownAdjectiveOffer = (subject, adjective) => ({
|
|
6513
|
-
text: `I don't know anything about "${subject}" yet — teach me directly, e.g. "remember that ${subject
|
|
6783
|
+
text: `I don't know anything about "${subject}" yet — teach me directly, e.g. "remember that ${teachableSubjectOf(subject)} is ${adjective}".`,
|
|
6514
6784
|
replace: true,
|
|
6515
6785
|
});
|
|
6516
6786
|
/** WHOLE-STORE recall: "what did i tell you [last time]",
|
|
@@ -7030,7 +7300,14 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7030
7300
|
const hit = isa
|
|
7031
7301
|
.filter((f) => subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7032
7302
|
.sort(byTrust)[0];
|
|
7033
|
-
|
|
7303
|
+
// A STORED NEGATIVE ("john is not a man") is a source disagreeing, so it is
|
|
7304
|
+
// 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`.
|
|
7306
|
+
const negHit = rows
|
|
7307
|
+
.filter((f) => f.predicate === NEG_SUBCLASS_PREDICATE && subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
7308
|
+
.sort(byTrust)[0];
|
|
7309
|
+
const polarityReply = isaPolarityReply(hit, negHit);
|
|
7310
|
+
if (polarityReply) return polarityReply;
|
|
7034
7311
|
// CLASS↔INSTANCE BRIDGE: when X resolves to a graph entity, its
|
|
7035
7312
|
// inherits chain's superclass LABELS are subject candidates too — a taught
|
|
7036
7313
|
// "controller ⊑ handler" composes with a graph "TaskController inherits
|
|
@@ -7184,7 +7461,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7184
7461
|
// missed — check whether X, having taught-P'd something of a taught type
|
|
7185
7462
|
// (lifted through that type's FULL ⊑-ancestor closure), satisfies a
|
|
7186
7463
|
// TAUGHT someValuesFrom restriction declared over that SAME (property,
|
|
7187
|
-
// type) pair — the restriction CLASS itself entailed (OWL 2 RL Table
|
|
7464
|
+
// type) pair — the restriction CLASS itself entailed (OWL 2 RL Table 6's
|
|
7188
7465
|
// cls-svf1), via syllogise.mjs's deriveSomeValuesFromApplication, LIVE and
|
|
7189
7466
|
// READ-ONLY (same discipline as the cax-dw chase just above: nothing is
|
|
7190
7467
|
// written; syllogise()'s materializing batch pass is the persisting
|
|
@@ -7668,7 +7945,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7668
7945
|
// originally-intended case here) still gets this receipt exactly as
|
|
7669
7946
|
// before, since envelope.parsed is null for those adjectives.
|
|
7670
7947
|
if (rows.some(subjectMatch) && !envelope?.parsed) {
|
|
7671
|
-
return { text: `I don't have a fact saying ${subject
|
|
7948
|
+
return { text: `I don't have a fact saying ${teachableSubjectOf(subject)} is ${adjective}.`, replace: true };
|
|
7672
7949
|
}
|
|
7673
7950
|
// Without this, "is the checkout flow
|
|
7674
7951
|
// deprecated" as a genuinely FIRST-EVER question about a subject tmct
|
|
@@ -8486,11 +8763,31 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
8486
8763
|
// updates too — mirrors the focus updates the ordinary ask() path does.
|
|
8487
8764
|
const ent = await resolveEntity(graph, term);
|
|
8488
8765
|
return { text, ent };
|
|
8489
|
-
} catch {
|
|
8766
|
+
} catch (e) {
|
|
8767
|
+
// tmct_describe answers a concept from memory/corpus facts whenever the
|
|
8768
|
+
// code map holds nothing for it — but dispatchTool loads the graph BEFORE
|
|
8769
|
+
// the handler runs, so on an empty one the handler never gets to reach its
|
|
8770
|
+
// own fall-through. Reach it here instead: "tell me about a dog" is a
|
|
8771
|
+
// question about a dog, and the code graph's emptiness is no answer to it.
|
|
8772
|
+
if (e?.emptyGraph) return describeFromMemoryFacts(term, config);
|
|
8490
8773
|
return null; // unresolvable term — decline, the ordinary wall stands unchanged
|
|
8491
8774
|
}
|
|
8492
8775
|
}
|
|
8493
8776
|
|
|
8777
|
+
/** tmct_describe's OWN memory/corpus fall-through (tools/memory-fallthrough.mjs),
|
|
8778
|
+
* called directly for a session whose code graph is empty. Same rows, same
|
|
8779
|
+
* renderer, same provenance the handler itself would have cited. */
|
|
8780
|
+
async function describeFromMemoryFacts(term, config) {
|
|
8781
|
+
if (!config) return null;
|
|
8782
|
+
try {
|
|
8783
|
+
const { memoryFactRows, renderMemoryDefinition } = await import("../tools/memory-fallthrough.mjs");
|
|
8784
|
+
const text = renderMemoryDefinition(await memoryFactRows(config), term);
|
|
8785
|
+
return text ? { text, ent: null } : null;
|
|
8786
|
+
} catch {
|
|
8787
|
+
return null;
|
|
8788
|
+
}
|
|
8789
|
+
}
|
|
8790
|
+
|
|
8494
8791
|
/** COMPARE — a scoped
|
|
8495
8792
|
* v1: "how is X different from Y", "how does X differ from Y", "compare X and
|
|
8496
8793
|
* Y"/"compare X with/to Y", "what's the difference between X and Y". Closed
|
|
@@ -8849,7 +9146,18 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
8849
9146
|
// The verbless voicing carries every capture but the verb, so it folds into
|
|
8850
9147
|
// the frame below once the store names the verb — same spec, same
|
|
8851
9148
|
// confirmation, same fold as its verbed twin.
|
|
8852
|
-
|
|
9149
|
+
let verblessGoal = goalMatch ? null : q.match(GOAL_TEACH_VERBLESS_RE);
|
|
9150
|
+
// The imperative voicing ("get all the disks onto peg-c") folds into the same
|
|
9151
|
+
// verbless resolution: singularize the class term ("disks"→"disk"), read the
|
|
9152
|
+
// universal off the quantifier, and normalize the motion preposition to the
|
|
9153
|
+
// static one a location fact is stored under ("onto"→"on").
|
|
9154
|
+
if (!goalMatch && !verblessGoal) {
|
|
9155
|
+
const imperative = q.match(GOAL_TEACH_IMPERATIVE_RE);
|
|
9156
|
+
if (imperative) {
|
|
9157
|
+
const prep = { onto: "on", into: "in", upon: "on" }[imperative[3].toLowerCase()] ?? imperative[3].toLowerCase();
|
|
9158
|
+
verblessGoal = [imperative[0], imperative[1] ? "every" : "", singularizeSurface(imperative[2]), prep, imperative[4]];
|
|
9159
|
+
}
|
|
9160
|
+
}
|
|
8853
9161
|
if (verblessGoal) {
|
|
8854
9162
|
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
8855
9163
|
const { factRows, domain } = await loadPlanContext(memoryDir);
|
|
@@ -9073,6 +9381,90 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
9073
9381
|
};
|
|
9074
9382
|
}
|
|
9075
9383
|
|
|
9384
|
+
/** Plan follow-up questions ("what is the next move", "how many moves", "why
|
|
9385
|
+
* that move") answered off the ACTIVE plan, and board-state questions ("is X
|
|
9386
|
+
* clear", "what rests on X", "where is X") answered off the CURRENT board (the
|
|
9387
|
+
* latest @stepK snapshot, or the taught board before any step). Returns
|
|
9388
|
+
* { text, deduced, note } or null — null when the query is neither shape, or
|
|
9389
|
+
* when no plan/board stands to answer from, so an honest miss stands cold.
|
|
9390
|
+
* This keeps a read from contradicting the plan's own board@stepK line: after
|
|
9391
|
+
* "next" moves a piece, "what rests on X" reflects the snapshot, not the stale
|
|
9392
|
+
* pre-plan facts. Clearness is derived, never stored — a piece is clear iff
|
|
9393
|
+
* nothing rests on it on the current board. */
|
|
9394
|
+
async function planFollowUpAnswer(query, { memoryDir, planHolder }) {
|
|
9395
|
+
const q = String(query).trim();
|
|
9396
|
+
const ps = planHolder?.state;
|
|
9397
|
+
const activePlan = ps && Array.isArray(ps.actions) && ps.actions.length;
|
|
9398
|
+
|
|
9399
|
+
if (PLAN_WHAT_NEXT_RE.test(q)) {
|
|
9400
|
+
if (!activePlan) return null;
|
|
9401
|
+
if (ps.done || ps.cursor >= ps.actions.length) {
|
|
9402
|
+
return { text: `the plan is complete — all ${ps.actions.length} moves are made.`, deduced: "name the next planned move (plan complete)", note: "PLAN FOLLOW-UP — next move: plan already complete" };
|
|
9403
|
+
}
|
|
9404
|
+
return { text: `the next move is move ${ps.cursor + 1} of ${ps.actions.length}: ${ps.actions[ps.cursor].label}. Say "next" to make it.`, deduced: "name the next planned move", note: "PLAN FOLLOW-UP — next move read from the active plan" };
|
|
9405
|
+
}
|
|
9406
|
+
if (PLAN_MOVE_COUNT_RE.test(q)) {
|
|
9407
|
+
if (!activePlan) return null;
|
|
9408
|
+
const total = ps.actions.length;
|
|
9409
|
+
const remaining = Math.max(0, total - ps.cursor);
|
|
9410
|
+
return { text: remaining === total ? `${total} move${total === 1 ? "" : "s"} in the plan.` : `${total} move${total === 1 ? "" : "s"} in the plan, ${remaining} still to make.`, deduced: "count the moves in the active plan", note: "PLAN FOLLOW-UP — move count from the active plan" };
|
|
9411
|
+
}
|
|
9412
|
+
if (PLAN_WHY_MOVE_RE.test(q)) {
|
|
9413
|
+
if (!activePlan) return null;
|
|
9414
|
+
const idx = ps.cursor < ps.actions.length ? ps.cursor : ps.actions.length - 1;
|
|
9415
|
+
const line = ps.stepGoals?.[idx] ?? `${ps.actions[idx].label} (step ${idx + 1} of ${ps.actions.length})`;
|
|
9416
|
+
return { text: `${line} — it is this step's move on the shortest path.`, deduced: "explain the next planned move", note: "PLAN FOLLOW-UP — why-move from the active plan's step goals" };
|
|
9417
|
+
}
|
|
9418
|
+
|
|
9419
|
+
const clear = q.match(IS_CLEAR_RE);
|
|
9420
|
+
const rev = clear ? null : q.match(BOARD_REVERSE_LOC_RE);
|
|
9421
|
+
const fwd = clear || rev ? null : q.match(BOARD_FORWARD_LOC_RE);
|
|
9422
|
+
const where = clear || rev || fwd ? null : q.match(BOARD_WHERE_RE);
|
|
9423
|
+
if (!clear && !rev && !fwd && !where) return null;
|
|
9424
|
+
if (!memoryDir) return null;
|
|
9425
|
+
|
|
9426
|
+
let ctx;
|
|
9427
|
+
try { ctx = await loadPlanContext(memoryDir); } catch { return null; }
|
|
9428
|
+
const { domain, state } = ctx;
|
|
9429
|
+
if (!domain.actions.length || !state.length) return null; // no board — the honest miss stands
|
|
9430
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
9431
|
+
const individuals = new Set(Object.values(domain.classMembers || {}).flat());
|
|
9432
|
+
|
|
9433
|
+
if (clear) {
|
|
9434
|
+
const x = normFactTerm(clear[1]);
|
|
9435
|
+
if (!individuals.has(x)) return null;
|
|
9436
|
+
const on = state.filter((r) => r.object === x);
|
|
9437
|
+
return on.length
|
|
9438
|
+
? { 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
|
+
: { 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
|
+
}
|
|
9441
|
+
if (where || fwd) {
|
|
9442
|
+
const x = normFactTerm((where ?? fwd)[1]);
|
|
9443
|
+
if (!individuals.has(x)) return null;
|
|
9444
|
+
const rows = state.filter((r) => r.subject === x);
|
|
9445
|
+
if (!rows.length) return where
|
|
9446
|
+
? { text: `nothing on the current board says where ${x} is.`, deduced: "read the current board (where a piece is)", note: "BOARD — forward locative, no row for the piece" }
|
|
9447
|
+
: null; // a verb-specific forward miss falls to the ordinary reader
|
|
9448
|
+
return { text: rows.map(factPhrase).join("; "), deduced: "read the current board (where a piece is)", note: "BOARD — forward locative from the current board" };
|
|
9449
|
+
}
|
|
9450
|
+
// reverse: "what rests on X" / "what is on X"
|
|
9451
|
+
const verb = rev[1].toLowerCase();
|
|
9452
|
+
const prep = rev[2].toLowerCase();
|
|
9453
|
+
const x = normFactTerm(rev[3].replace(/^(?:an?|the)\s+/i, "").trim());
|
|
9454
|
+
if (!individuals.has(x)) return null;
|
|
9455
|
+
const copula = /^(?:is|are|'s)$/.test(verb);
|
|
9456
|
+
let predicate = null;
|
|
9457
|
+
if (!copula) {
|
|
9458
|
+
predicate = await generalVerbPredicate(verb);
|
|
9459
|
+
if (/^mgx:[a-z]+$/.test(predicate)) predicate = `${predicate}-${prep}`;
|
|
9460
|
+
}
|
|
9461
|
+
const hits = state.filter((r) => r.object === x && (copula || r.predicate === predicate));
|
|
9462
|
+
const emptyPhrase = predicate ? predicatePhrase(predicate) : "is on";
|
|
9463
|
+
return hits.length
|
|
9464
|
+
? { text: hits.map(factPhrase).join("; "), deduced: "read the current board (what rests on a piece)", note: "BOARD — reverse locative from the current board" }
|
|
9465
|
+
: { text: `nothing ${emptyPhrase} ${x} on the current board.`, deduced: "read the current board (what rests on a piece)", note: "BOARD — reverse locative, nothing on the current board" };
|
|
9466
|
+
}
|
|
9467
|
+
|
|
9076
9468
|
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null }) {
|
|
9077
9469
|
const ts = new Date().toISOString();
|
|
9078
9470
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
@@ -9151,12 +9543,19 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9151
9543
|
} catch (e) {
|
|
9152
9544
|
const thrown = String(e?.message || e);
|
|
9153
9545
|
// A graph-less session's ask dispatch fails reading the never-configured
|
|
9154
|
-
// graph artifact
|
|
9155
|
-
//
|
|
9156
|
-
//
|
|
9157
|
-
//
|
|
9158
|
-
//
|
|
9159
|
-
|
|
9546
|
+
// graph artifact, or loads one holding nothing (e.emptyGraph — the first
|
|
9547
|
+
// turn of a fresh session, before the conversation has folded anything in).
|
|
9548
|
+
// Either way it's an internal error string, not an answer. Swap in an
|
|
9549
|
+
// honest wall; the teach/fact/vocabulary lanes below still get their turn
|
|
9550
|
+
// and replace it whenever they can store or answer instead. A missing
|
|
9551
|
+
// config gets the same wall: with no config at all, no dispatch could ever
|
|
9552
|
+
// have loaded a graph, whatever the internal error spelled.
|
|
9553
|
+
//
|
|
9554
|
+
// The session hands this lane a KNOWN-EMPTY graph object rather than null
|
|
9555
|
+
// on that first turn, so the test is noCodeGraph, not `!graph`: an empty
|
|
9556
|
+
// graph is as unusable as an absent one, and reporting its emptiness to
|
|
9557
|
+
// someone asking about a dog answers a question they never asked.
|
|
9558
|
+
answer = (!graph || noCodeGraph(graph)) && (!config || e?.emptyGraph || /^cannot read graph artifact\b/.test(thrown))
|
|
9160
9559
|
? "I can't answer that as a code question — no code graph is loaded in this session. "
|
|
9161
9560
|
+ "I can still remember and answer taught facts (try \"every disk is a game piece\"), "
|
|
9162
9561
|
+ "or run `tmct init` in a repo to index one."
|
|
@@ -9295,7 +9694,9 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9295
9694
|
if (memoryDir) {
|
|
9296
9695
|
const whatElse = await whatElseAnswer(memoryDir, query, last);
|
|
9297
9696
|
if (whatElse) {
|
|
9298
|
-
|
|
9697
|
+
// A cold "what else" carries miss:true — it resolved no subject, so the
|
|
9698
|
+
// turn is a miss in better words, exactly like the isa ladder's closers.
|
|
9699
|
+
answer = whatElse.text; via = "fact:what-else"; recordMiss = whatElse.miss ?? false; handled = true;
|
|
9299
9700
|
if (whatElse.pending) factPending = whatElse.pending;
|
|
9300
9701
|
deduced = "surface additional remembered facts beyond the primary definition";
|
|
9301
9702
|
note(trace, "lane: (0) WHAT ELSE — \"what else is/about X\" recognized off the raw query, before the relaxation cascade could quietly drop \"else\" and reduce it to a plain \"what is X\"");
|
|
@@ -10350,6 +10751,57 @@ function renderAmbiguousAssert(line, ambiguous, normFactTerm) {
|
|
|
10350
10751
|
* committing to the first. Returns null for the overwhelming majority of
|
|
10351
10752
|
* sentences, so every single-reading sentence renders byte-identically to
|
|
10352
10753
|
* the unchanged parseAce path below. */
|
|
10754
|
+
/** Does this sentence match the general-verb teach frame on its own terms —
|
|
10755
|
+
* reusing generalVerbTeach's OWN closed guards, so the split gate and the lane
|
|
10756
|
+
* it feeds can never disagree about which sentences that lane accepts. */
|
|
10757
|
+
function matchesGeneralVerbTeachFrame(sentence) {
|
|
10758
|
+
if (GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(sentence)) return false;
|
|
10759
|
+
const m = sentence.match(GENERAL_VERB_TEACH_RE);
|
|
10760
|
+
if (!m) return false;
|
|
10761
|
+
const [, subject, verb] = m;
|
|
10762
|
+
return !GENERAL_VERB_EXCLUDE_RE.test(verb)
|
|
10763
|
+
&& !GENERAL_VERB_NOT_A_VERB_RE.test(verb)
|
|
10764
|
+
&& !GENERAL_VERB_DETERMINER_RE.test(subject)
|
|
10765
|
+
&& !GENERAL_VERB_IMPERATIVE_SUBJECT_RE.test(subject);
|
|
10766
|
+
}
|
|
10767
|
+
|
|
10768
|
+
/** Does this sentence stand alone as a teach — a clean ACE triple, a taxonomy
|
|
10769
|
+
* declaration, the comparative frame, or the general-verb frame? A question
|
|
10770
|
+
* never counts.
|
|
10771
|
+
*
|
|
10772
|
+
* Each entry names a teach lane that accepts the sentence ALONE, so the set
|
|
10773
|
+
* here has to track that lane list or a line of real teach sentences goes to
|
|
10774
|
+
* the parser glued together. The comparative was missing, and the cost was on
|
|
10775
|
+
* a shipped surface: data/games/hanoi-3.txt's own board line
|
|
10776
|
+
* ("disk-1 is smaller than disk-2. disk-1 is smaller than disk-3.") stored
|
|
10777
|
+
* NOTHING, so disk-1 could never move and the recipe's promised solution did
|
|
10778
|
+
* not exist. The sibling lines on either side of it split correctly, which is
|
|
10779
|
+
* what made it invisible. */
|
|
10780
|
+
function sentenceTeachesAlone(sentence, parseAce, lex) {
|
|
10781
|
+
const s = String(sentence).trim();
|
|
10782
|
+
if (!s || s.includes("?")) return false;
|
|
10783
|
+
const parse = parseAce(s, lex);
|
|
10784
|
+
if (parse && parse.triples?.length && !parse.residue?.length) return true;
|
|
10785
|
+
return DECLARATIVE_KIND_OF_RE.test(s) || COMPARATIVE_TEACH_RE.test(s) || matchesGeneralVerbTeachFrame(s);
|
|
10786
|
+
}
|
|
10787
|
+
|
|
10788
|
+
/** Does every sentence of a multi-sentence line teach on its own? Then the line
|
|
10789
|
+
* is a teach line, and each sentence belongs in its own turn: handed over
|
|
10790
|
+
* glued, the first sentence's teach frame captures all the others as its
|
|
10791
|
+
* object ("disk-1 rests on disk-2. disk-2 rests on disk-3." stores an object of
|
|
10792
|
+
* "on disk-2. disk-2 rests on disk-3"). Any parse failure answers false and
|
|
10793
|
+
* leaves the unsplit line to the ordinary lanes. */
|
|
10794
|
+
async function everySentenceTeaches(sentences, lexicon) {
|
|
10795
|
+
try {
|
|
10796
|
+
const { parseAce } = await import("../domain/grammar/ace.mjs");
|
|
10797
|
+
let lex = lexicon;
|
|
10798
|
+
if (!lex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); lex = loadLexicon(); }
|
|
10799
|
+
return sentences.every((sentence) => sentenceTeachesAlone(sentence, parseAce, lex));
|
|
10800
|
+
} catch {
|
|
10801
|
+
return false;
|
|
10802
|
+
}
|
|
10803
|
+
}
|
|
10804
|
+
|
|
10353
10805
|
async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, cache = null }) {
|
|
10354
10806
|
try {
|
|
10355
10807
|
const { parseAce, parseAceAmbiguous } = await import("../domain/grammar/ace.mjs");
|
|
@@ -10463,6 +10915,22 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
10463
10915
|
// `pending`, so the remainder is naturally cleared — no stale continuation. ----
|
|
10464
10916
|
const PAGE = 32;
|
|
10465
10917
|
const MORE_RE = /^(?:more|show more|see more|the rest|next|continue|go on)\b[.!?]*$/i;
|
|
10918
|
+
|
|
10919
|
+
/** "what would break if I change X" — the impact closure asked for in the
|
|
10920
|
+
* words people use, rather than as /impact. Sibling of normalize.mjs's
|
|
10921
|
+
* COUNTERFACTUAL_RE ("if X were deleted, what would break"), which states the
|
|
10922
|
+
* same counterfactual in the other clause order and compiles to the reverse
|
|
10923
|
+
* import closure; this shape names a CHANGE rather than a deletion, so it
|
|
10924
|
+
* answers with the impact closure /impact itself renders. The verbs are a
|
|
10925
|
+
* closed set on both sides — no general "any verb in a conditional" fit. */
|
|
10926
|
+
const IMPACT_PARAPHRASE_RE = new RegExp(
|
|
10927
|
+
"^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
|
+
+ "\\s+if\\s+(?:i|we|you|one|someone)\\s+"
|
|
10930
|
+
+ "(?:changed?|modif(?:y|ied)|edits?|edited|touch(?:es|ed)?|updates?|updated|alters?|altered)"
|
|
10931
|
+
+ "\\s+(?:the\\s+)?(.+?)[?.!\\s]*$",
|
|
10932
|
+
"i",
|
|
10933
|
+
);
|
|
10466
10934
|
const joinList = (a) => (a.length > 1 ? `${a.slice(0, -1).join(", ")} and ${a[a.length - 1]}` : (a[0] ?? ""));
|
|
10467
10935
|
|
|
10468
10936
|
/** Render the next page of a held remainder (pending: {items:[str], noun}). Returns a
|
|
@@ -10574,21 +11042,25 @@ function coldPronounDeclineText(query) {
|
|
|
10574
11042
|
return `not sure what "${m[2].toLowerCase()}" refers to yet — name the subject directly, e.g. "what is a <name>".`;
|
|
10575
11043
|
}
|
|
10576
11044
|
|
|
10577
|
-
/** The subject of the
|
|
10578
|
-
* binding ("what is a dog" → "can it bark"). Fact answers render
|
|
10579
|
-
* "<subject> <phrase> <object> (source: …)", optionally behind a
|
|
10580
|
-
* "no — "/"you told me: " prefix — so a 1–2 word leading subject
|
|
10581
|
-
* by a phrase-table verb is extractable without any NLP. Anything
|
|
10582
|
-
* (code answers, walls, conversational text) returns null and no
|
|
11045
|
+
/** The subject of the last GROUNDED turn's first fact line, for vocabulary
|
|
11046
|
+
* pronoun binding ("what is a dog" → "can it bark"). Fact answers render
|
|
11047
|
+
* rigidly — "<subject> <phrase> <object> (source: …)", optionally behind a
|
|
11048
|
+
* "yes — "/"no — "/"you told me: " prefix — so a 1–2 word leading subject
|
|
11049
|
+
* followed by a phrase-table verb is extractable without any NLP. Anything
|
|
11050
|
+
* else (code answers, walls, conversational text) returns null and no
|
|
10583
11051
|
* substitution happens.
|
|
10584
11052
|
*
|
|
11053
|
+
* It reads `grounded`, not `answer`, so an intervening miss leaves the
|
|
11054
|
+
* standing referent alone rather than stranding every pronoun behind it.
|
|
11055
|
+
*
|
|
10585
11056
|
* A pronoun never binds. An honest miss opens first-person ("I can't confirm
|
|
10586
11057
|
* that — …"), which fits the subject+verb shape exactly, so without the
|
|
10587
|
-
* isTeachPronoun check
|
|
10588
|
-
* animal"
|
|
10589
|
-
* subject here than in the teach frames TEACH_PRONOUNS already
|
|
11058
|
+
* isTeachPronoun check a miss reaching here would lend "I" to the next turn
|
|
11059
|
+
* and "is it an animal" would be looked up as "is I an animal". A pronoun is
|
|
11060
|
+
* no more a fact subject here than in the teach frames TEACH_PRONOUNS already
|
|
11061
|
+
* guards. */
|
|
10590
11062
|
function vocabAntecedentFrom(last) {
|
|
10591
|
-
const first = String(last?.
|
|
11063
|
+
const first = String(last?.grounded || "").split("\n")[0]
|
|
10592
11064
|
.replace(/^(?:yes|no) — /i, "")
|
|
10593
11065
|
.replace(/^you told me: /i, "");
|
|
10594
11066
|
const m = first.match(/^([a-z][\w'-]*(?:\s+[a-z][\w'-]*)?)\s+(?:is|are|has|can|causes|wants|requires|involves|means|begins|ends)\b/i);
|
|
@@ -10666,7 +11138,21 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
10666
11138
|
// runAsk's own effectiveQuery (set only when discourseRewrite substituted
|
|
10667
11139
|
// a new subject and produced a genuine non-miss answer) takes over as the
|
|
10668
11140
|
// continuation base for the NEXT turn's own discourseRewrite.
|
|
10669
|
-
|
|
11141
|
+
//
|
|
11142
|
+
// `grounded` is the last answer that actually ANSWERED, carried forward
|
|
11143
|
+
// across misses. It is what a pronoun's referent binds to
|
|
11144
|
+
// (vocabAntecedentFrom): a reader's misses come in clusters, and reading
|
|
11145
|
+
// the referent off a wall stranded every pronoun after one stray line.
|
|
11146
|
+
//
|
|
11147
|
+
// `answer` must keep recording the miss regardless — the repeat-shortening
|
|
11148
|
+
// walls compare consecutive answers through it, so a miss that declined to
|
|
11149
|
+
// record itself would make every wall look like a first offence.
|
|
11150
|
+
const nextLast = {
|
|
11151
|
+
query: finished.effectiveQuery ?? line,
|
|
11152
|
+
answer: finished.answer,
|
|
11153
|
+
detail: finished.detail ?? null,
|
|
11154
|
+
grounded: finished.record?.miss ? (last?.grounded ?? null) : finished.answer,
|
|
11155
|
+
};
|
|
10670
11156
|
// Goal/canonical lines append onto the PRE-narration `finished` result
|
|
10671
11157
|
// `nextLast` was captured from, so a narrated turn still gets both short
|
|
10672
11158
|
// lines up top plus the full trace block after.
|
|
@@ -10702,6 +11188,23 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
10702
11188
|
return rec;
|
|
10703
11189
|
}
|
|
10704
11190
|
|
|
11191
|
+
// PLAN FOLLOW-UP + BOARD — "what is the next move", "how many moves", "why
|
|
11192
|
+
// that move" read the active plan; "is X clear", "what rests on X",
|
|
11193
|
+
// "where is X" read the CURRENT board. Placed before answerCount (which owns
|
|
11194
|
+
// "how many …") and the ask engine, so a plan/board answer never loses to a
|
|
11195
|
+
// code-graph miss. Returns null with no plan/board standing, so nothing
|
|
11196
|
+
// changes for a cold session — an honest miss still stands.
|
|
11197
|
+
if (memoryDir) {
|
|
11198
|
+
const follow = await planFollowUpAnswer(workingLine, { memoryDir, planHolder });
|
|
11199
|
+
if (follow) {
|
|
11200
|
+
note(trace, `goal: ${follow.deduced}`);
|
|
11201
|
+
note(trace, `lane: ${follow.note}`);
|
|
11202
|
+
const rec = withLast(plainTurn(workingLine, follow.text, { via: "plan", focus }), follow.deduced);
|
|
11203
|
+
rec.planState = planHolder.state;
|
|
11204
|
+
return rec;
|
|
11205
|
+
}
|
|
11206
|
+
}
|
|
11207
|
+
|
|
10705
11208
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
10706
11209
|
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
10707
11210
|
// to the ordinary path (an honest miss), never a pretend page.
|
|
@@ -10711,16 +11214,37 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
10711
11214
|
return withLast(morePage(workingLine, ctx), "continue viewing a previous long listing");
|
|
10712
11215
|
}
|
|
10713
11216
|
|
|
10714
|
-
//
|
|
10715
|
-
//
|
|
10716
|
-
//
|
|
10717
|
-
//
|
|
10718
|
-
|
|
11217
|
+
// "what would break if I change X" / "what breaks if I touch X" — the impact
|
|
11218
|
+
// closure, in the words people actually ask for it in. With no frame of its
|
|
11219
|
+
// own the line reached the grammar with "break"/"breaks if i" worn away as
|
|
11220
|
+
// filler, and the residue ("break I") read as a subject for the history
|
|
11221
|
+
// lane's `touches` — answering who last touched a file to a question about
|
|
11222
|
+
// what a change to it would reach. /impact's own closure is the answer, and
|
|
11223
|
+
// its wording ("Impact of changing X") already says the change is
|
|
11224
|
+
// hypothetical.
|
|
11225
|
+
const impactParaphrase = workingLine.match(IMPACT_PARAPHRASE_RE);
|
|
11226
|
+
if (impactParaphrase) {
|
|
11227
|
+
const impactDeduced = "understand what a change to this module would reach (impact closure)";
|
|
11228
|
+
note(trace, `goal: ${impactDeduced}`);
|
|
11229
|
+
note(trace, `lane: IMPACT_PARAPHRASE_RE matched -> /impact ${impactParaphrase[1].trim()}`);
|
|
11230
|
+
return withLast(await runCommand(`/impact ${impactParaphrase[1].trim()}`, ctx), impactDeduced);
|
|
11231
|
+
}
|
|
11232
|
+
|
|
11233
|
+
// Multi-sentence pre-split — one message carrying several sentences
|
|
11234
|
+
// ("disk-1 rests on disk-2. … the goal is that …. solve it.") runs each
|
|
11235
|
+
// sentence as its own nested turn, threading focus/last/planState through,
|
|
11236
|
+
// and answers with the final turn's result behind brief receipts. Fires
|
|
11237
|
+
// when the line ends in a plan trigger, or when every sentence is a
|
|
11238
|
+
// self-contained teach: either way each sentence has a lane of its own, so
|
|
11239
|
+
// none of them reaches the parser glued to its neighbours.
|
|
11240
|
+
if (!_noSplit && memoryDir && carriesASentenceBoundary(workingLine)) {
|
|
10719
11241
|
const sentences = splitSentences(workingLine);
|
|
10720
11242
|
if (sentences.length > 1) {
|
|
10721
11243
|
const lastSentence = sentences[sentences.length - 1];
|
|
10722
|
-
|
|
10723
|
-
||
|
|
11244
|
+
const endsInPlanTrigger = PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence)
|
|
11245
|
+
|| GOAL_TEACH_INFINITIVE_RE.test(lastSentence) || GOAL_TEACH_VERBLESS_RE.test(lastSentence)
|
|
11246
|
+
|| LEGAL_MOVES_RE.test(lastSentence);
|
|
11247
|
+
if (endsInPlanTrigger || await everySentenceTeaches(sentences, lexicon)) {
|
|
10724
11248
|
let f = focus; let l = last; let ps = planHolder.state;
|
|
10725
11249
|
const receipts = [];
|
|
10726
11250
|
let finalRec = null;
|
|
@@ -10735,8 +11259,22 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
10735
11259
|
finalRec = r;
|
|
10736
11260
|
receipts.push(String(r.answer ?? "").split("\n")[0]);
|
|
10737
11261
|
}
|
|
10738
|
-
|
|
10739
|
-
|
|
11262
|
+
// When a plan trigger closes the line, the final sentence is the payload
|
|
11263
|
+
// (the plan) and the earlier teaches are brief bulleted receipts. When
|
|
11264
|
+
// every sentence teaches, the final one is a teach too, so it earns a
|
|
11265
|
+
// bullet like its siblings — otherwise it renders unbulleted and trails
|
|
11266
|
+
// a stray "Goal (inferred)" line the bulleted ones already dropped. Its
|
|
11267
|
+
// goal-line tail (everything after the receipt's first line) is kept once.
|
|
11268
|
+
let answer;
|
|
11269
|
+
if (endsInPlanTrigger) {
|
|
11270
|
+
const receiptLines = receipts.slice(0, -1).map((t) => `• ${t}`).join("\n");
|
|
11271
|
+
answer = receiptLines ? `${receiptLines}\n\n${finalRec.answer}` : finalRec.answer;
|
|
11272
|
+
} else {
|
|
11273
|
+
const bullets = receipts.map((t) => `• ${t}`).join("\n");
|
|
11274
|
+
const goalTail = String(finalRec.answer ?? "").split("\n").slice(1).join("\n").trim();
|
|
11275
|
+
answer = goalTail ? `${bullets}\n\n${goalTail}` : bullets;
|
|
11276
|
+
}
|
|
11277
|
+
const combined = { ...finalRec, answer };
|
|
10740
11278
|
combined.planState = ps;
|
|
10741
11279
|
combined.focus = f;
|
|
10742
11280
|
combined.last = l;
|