@polycode-projects/the-mechanical-code-talker 6.0.19 → 6.0.21
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 +20 -23
- package/bin/tmct.mjs +16 -33
- package/corpus/LICENSES.json +0 -21
- package/corpus/README.md +10 -13
- package/corpus/reference/manifest.json +19 -19
- package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-08.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-17.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
- package/corpus/tier2/generate.mjs +6 -142
- package/corpus/tier2/manifest.json +0 -42
- package/package.json +4 -4
- package/src/adapters/corpus/child-seed.mjs +74 -0
- package/src/adapters/corpus/conceptnet.mjs +45 -26
- package/src/adapters/memory/blocks.mjs +7 -1
- package/src/adapters/memory/core.mjs +453 -103
- package/src/adapters/memory/corpus-bands.mjs +33 -10
- package/src/adapters/memory/inspect.mjs +24 -5
- package/src/adapters/memory/rows.mjs +106 -9
- package/src/adapters/memory/shacl.mjs +10 -3
- package/src/domain/ask.mjs +27 -10
- package/src/domain/cli-verbs.mjs +3 -4
- package/src/domain/completions/group.mjs +8 -3
- package/src/domain/completions/infer.mjs +7 -2
- package/src/domain/completions/prune.mjs +5 -1
- package/src/domain/completions/rank.mjs +7 -2
- package/src/domain/digest/compose.mjs +5 -1
- package/src/domain/digest/select.mjs +12 -6
- package/src/domain/domain.mjs +15 -8
- package/src/domain/el-classify.mjs +11 -2
- package/src/domain/fact-phrase.mjs +86 -4
- package/src/domain/hash.mjs +9 -0
- package/src/domain/memory/bias.mjs +8 -4
- package/src/domain/memory/capability.mjs +12 -6
- package/src/domain/memory/fact-order.mjs +29 -0
- package/src/domain/memory/resolution.mjs +3 -0
- package/src/domain/news-feed.mjs +422 -56
- package/src/domain/reference-pack.mjs +5 -0
- package/src/domain/sense-scope.mjs +116 -0
- package/src/domain/sense-split.mjs +1 -1
- package/src/domain/syllogise.mjs +21 -13
- package/src/domain/tableau.mjs +23 -14
- package/src/domain/worlds-pack.mjs +5 -1
- package/src/services/adventure-autoplay.mjs +6 -1
- package/src/services/adventure-editor.mjs +43 -21
- package/src/services/adventure-viz.mjs +26 -9
- package/src/services/adventure.mjs +40 -10
- package/src/services/chat.mjs +253 -113
- package/src/services/extensions.mjs +51 -58
- package/src/services/extract-facts.mjs +670 -95
- package/src/services/init.mjs +4 -4
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +4 -5
- package/src/services/mud-editor.mjs +40 -16
- package/src/services/mud-viz.mjs +8 -2
- package/src/services/mudiii-turn.mjs +5 -3
- package/src/services/mudiii-viz.mjs +8 -2
- package/src/services/news.mjs +277 -11
- package/src/services/research-viz.mjs +1 -1
- package/src/services/sprite-catalog-viz.mjs +10 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +6 -12
- package/src/surfaces/web/memory-ask-browser.bundle.js +152 -151
- package/src/surfaces/web/mud-browser-entry.mjs +7 -11
- package/src/surfaces/web/research-browser-entry.mjs +5 -2
- package/corpus/tier2/aws.jsonl +0 -39
- package/corpus/tier2/java.jsonl +0 -31
- package/corpus/tier2/python.jsonl +0 -30
package/src/services/chat.mjs
CHANGED
|
@@ -37,6 +37,12 @@ import { uuidv7 } from "../adapters/uuid.mjs";
|
|
|
37
37
|
import * as defaultSource from "../adapters/source.mjs";
|
|
38
38
|
import { loadTemplates, render as renderTemplate } from "../adapters/corpus/templates.mjs";
|
|
39
39
|
import { rankByBiasThenTrust } from "../domain/memory/bias.mjs";
|
|
40
|
+
import { compareFactsByContent } from "../domain/memory/fact-order.mjs";
|
|
41
|
+
// Bare-string ordering for anything read out of the store that isn't a whole
|
|
42
|
+
// fact row: a taught action family's name, a node id. Codepoint order, never
|
|
43
|
+
// localeCompare — fact-order.mjs states the reason, and it applies to a
|
|
44
|
+
// store-derived string just as much as to the row it came off.
|
|
45
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
40
46
|
import { HAS_A_PREDICATE, foldedFactRows as foldStoreFactRows, loadMemory as loadMemoryStore, normFactPredicate, normFactTerm as normFactTermStatic, readFactRows as readStoredFactRows, readRuleRows as readStoredRuleRows } from "../adapters/memory/core.mjs";
|
|
41
47
|
import { BACKEND_REJECTED_CODE, BACKEND_UNAVAILABLE_CODE } from "../adapters/memory/row-backend.mjs";
|
|
42
48
|
import {
|
|
@@ -79,6 +85,7 @@ import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
|
|
|
79
85
|
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
80
86
|
import { splitChoiceQuestion, routeChoiceRelation, lemmaFoldVariants, headNounOf, stemTopicCandidates, stemConstraintTerms } from "../domain/choice-question.mjs";
|
|
81
87
|
import { subClassParents, subClassChildren, descendantSet, ancestryChain, clusterSenses } from "../domain/sense-split.mjs";
|
|
88
|
+
import { buildSenseGate } from "../domain/sense-gate.mjs";
|
|
82
89
|
import { ANSWER_STOP_SET } from "../domain/hub-terms.mjs";
|
|
83
90
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
84
91
|
import { ENUMERATION_LANES, answerAssertsASet, retrievalMarkerLine } from "../domain/retrieval-marker.mjs";
|
|
@@ -1050,7 +1057,7 @@ async function answerMemoryClassQuery(memoryDir, query) {
|
|
|
1050
1057
|
try { ({ loadMemory, readFactRows } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
1051
1058
|
let mem;
|
|
1052
1059
|
try { mem = await loadMemory(memoryDir); } catch { return null; }
|
|
1053
|
-
const rows = cls === "Fact" ? readFactRows(mem) : null;
|
|
1060
|
+
const rows = cls === "Fact" ? readFactRows(mem).slice().sort(compareFactsByContent) : null;
|
|
1054
1061
|
const inds = rows || (mem.individuals || []).filter((i) => (i.class || "") === cls);
|
|
1055
1062
|
if (countM) return { text: `${inds.length} ${inds.length === 1 ? plural.replace(/s$/, "") : plural}.`, kind: "count" };
|
|
1056
1063
|
if (!inds.length) return { text: `I don't have any ${plural} stored yet.`, miss: true };
|
|
@@ -1071,13 +1078,6 @@ async function answerMemoryClassQuery(memoryDir, query) {
|
|
|
1071
1078
|
// stealing the phrasing before a member count ever runs.
|
|
1072
1079
|
const TAUGHT_CLASS_COUNT_RE = /^how\s+many\s+([a-z][\w-]*(?:\s+[a-z][\w-]*)*)\s*(.*)$/i;
|
|
1073
1080
|
|
|
1074
|
-
/** A fact row's deterministic identity for ordering. */
|
|
1075
|
-
const orderKeyOf = (f) => `${f.subject} ${f.predicate} ${f.object} ${f.provenance || ""}`;
|
|
1076
|
-
const orderKeyCompare = (a, b) => {
|
|
1077
|
-
const ka = orderKeyOf(a); const kb = orderKeyOf(b);
|
|
1078
|
-
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
1081
|
/** The store's subclass graph, both directions, built once per turn.
|
|
1082
1082
|
* Keyed on the rows array identity so a rebuilt row cache rebuilds the maps. */
|
|
1083
1083
|
function classGraphFor(rows, cache) {
|
|
@@ -1097,7 +1097,7 @@ function taughtMembersUnder(isa, children, variants, biasByBundle) {
|
|
|
1097
1097
|
for (const v of variants) for (const d of descendantSet(v, children)) classes.add(d);
|
|
1098
1098
|
const isDirect = (f) => variants.has(f.object);
|
|
1099
1099
|
const candidates = isa.filter((f) => isDirect(f) || classes.has(f.object));
|
|
1100
|
-
const keyed = uniqueFacts(candidates)
|
|
1100
|
+
const keyed = uniqueFacts(candidates);
|
|
1101
1101
|
const direct = rankByBiasThenTrust(keyed.filter(isDirect), biasByBundle);
|
|
1102
1102
|
const inherited = rankByBiasThenTrust(keyed.filter((f) => !isDirect(f)), biasByBundle);
|
|
1103
1103
|
return { direct, inherited, members: [...direct, ...inherited], classes };
|
|
@@ -1307,11 +1307,7 @@ async function answerCollectionContents(memoryDir, query, biasByBundle = {}, cac
|
|
|
1307
1307
|
const containerRaw = restricted ? restricted[2] : unrestricted[1];
|
|
1308
1308
|
const rows = await factRows(memoryDir, cache);
|
|
1309
1309
|
const containerVariants = factTermVariants(normFactTerm, containerRaw);
|
|
1310
|
-
|
|
1311
|
-
// taughtMembersUnder: rankByBiasThenTrust's own tiebreak is array index, so
|
|
1312
|
-
// an unsorted filter would render two peers' facts in their own arrival
|
|
1313
|
-
// order instead of one shared order.
|
|
1314
|
-
const members = rows.filter((f) => CONTAINMENT_PREDICATES.includes(f.predicate) && containerVariants.has(f.object)).sort(orderKeyCompare);
|
|
1310
|
+
const members = rows.filter((f) => CONTAINMENT_PREDICATES.includes(f.predicate) && containerVariants.has(f.object));
|
|
1315
1311
|
if (!members.length) return null; // no containment rows at all — answerMembershipList keeps this noun
|
|
1316
1312
|
const renderMembers = (list, noun) => {
|
|
1317
1313
|
const ranked = rankByBiasThenTrust(uniqueFacts(list), biasByBundle);
|
|
@@ -3625,43 +3621,56 @@ const MINT_ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
|
3625
3621
|
* zorp is a thing"), then chain the other new term off the now-grounded one. */
|
|
3626
3622
|
const GENERIC_ANCHOR_NOUNS = new Set(["thing", "concept", "object", "entity"]);
|
|
3627
3623
|
|
|
3628
|
-
/**
|
|
3629
|
-
*
|
|
3630
|
-
*
|
|
3631
|
-
*
|
|
3632
|
-
*
|
|
3633
|
-
*
|
|
3634
|
-
*
|
|
3635
|
-
*
|
|
3636
|
-
*
|
|
3637
|
-
*
|
|
3638
|
-
*
|
|
3639
|
-
*
|
|
3640
|
-
|
|
3641
|
-
|
|
3624
|
+
/** Source types treated as a real anchor for the isa-tier below: a reference
|
|
3625
|
+
* work's own claim that a term denotes a class, as against the operator's
|
|
3626
|
+
* own words. provenanceTagToSource (trust.mjs) assigns them:
|
|
3627
|
+
* corpus corpus:<bundle>/child:<pack>:<term>/world:<name>/mud:<character>
|
|
3628
|
+
* reference reference:<pack>:<article>@<revid>, the shipped
|
|
3629
|
+
* revision-pinned pack (reference-pack.mjs)
|
|
3630
|
+
* referenceLive research:<source>:<term>, a live KB-adapter lookup, and the
|
|
3631
|
+
* live-Wikipedia supplement — the news enrichment cycle's own
|
|
3632
|
+
* fetched definitions land here
|
|
3633
|
+
* Without that last band the cycle could define a term and still leave every
|
|
3634
|
+
* sentence naming it unreadable, so re-processing an article after a lookup
|
|
3635
|
+
* could never change its answer. reference sits beside referenceLive on its
|
|
3636
|
+
* own merit, not symmetry: it outranks referenceLive in SOURCE_PRIOR and is
|
|
3637
|
+
* where a preloaded bulk knowledge band would land, so anchoring the live
|
|
3638
|
+
* lookup while refusing the curated pinned pack would be incoherent.
|
|
3639
|
+
*
|
|
3640
|
+
* Named explicitly, rather than "everything except corpusWeak/web/...", so a
|
|
3641
|
+
* band added later has to be added here on purpose before it can anchor
|
|
3642
|
+
* anything. What stays out and why: corpusWeak only ever carries
|
|
3643
|
+
* mgx:relatedTo (already excluded by the predicate filter, named here so a
|
|
3644
|
+
* future corpus-weak isa row can't anchor silently); extracted/
|
|
3645
|
+
* optimisticExtract/web isa rows are readings of running prose rather than a
|
|
3646
|
+
* reference work's own statement, which is the line this set draws;
|
|
3647
|
+
* provider-sourced code symbols already ground via isGroundedTerm's own
|
|
3648
|
+
* resolveSymbol branch. */
|
|
3649
|
+
const ANCHOR_SOURCE_TYPES = new Set(["corpus", "reference", "referenceLive"]);
|
|
3650
|
+
const isAnchorRow = (f) => !!f.sourceTypes?.some((t) => ANCHOR_SOURCE_TYPES.has(t));
|
|
3642
3651
|
|
|
3643
3652
|
/** Every term that appears as the subject or object of an isa-family fact
|
|
3644
3653
|
* (MINT_ISA_PREDICATES), split into the two tiers the grounding checks below
|
|
3645
|
-
* need: TAUGHT (isOperatorTaught) and
|
|
3646
|
-
*
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3654
|
+
* need: TAUGHT (isOperatorTaught) and ANCHORED (isAnchorRow). A row that is
|
|
3655
|
+
* both (an operator teach merged onto an existing anchor-band row, same
|
|
3656
|
+
* triple) lands in both Sets — no special case needed. Membership only: no
|
|
3657
|
+
* ordering, no first-match, no wall clock, so the same fact set fed in any
|
|
3658
|
+
* order produces identical Sets. */
|
|
3650
3659
|
function buildIsaTermIndex(rows) {
|
|
3651
3660
|
const taught = new Set();
|
|
3652
|
-
const
|
|
3661
|
+
const anchored = new Set();
|
|
3653
3662
|
for (const f of rows) {
|
|
3654
3663
|
if (!MINT_ISA_PREDICATES.has(f.predicate)) continue;
|
|
3655
3664
|
const taughtRow = isOperatorTaught(f);
|
|
3656
|
-
const
|
|
3657
|
-
if (!taughtRow && !
|
|
3665
|
+
const anchorRow = isAnchorRow(f);
|
|
3666
|
+
if (!taughtRow && !anchorRow) continue;
|
|
3658
3667
|
for (const term of [f.subject, f.object]) {
|
|
3659
3668
|
if (!term) continue;
|
|
3660
3669
|
if (taughtRow) taught.add(term);
|
|
3661
|
-
if (
|
|
3670
|
+
if (anchorRow) anchored.add(term);
|
|
3662
3671
|
}
|
|
3663
3672
|
}
|
|
3664
|
-
return { taught,
|
|
3673
|
+
return { taught, anchored };
|
|
3665
3674
|
}
|
|
3666
3675
|
export { buildIsaTermIndex };
|
|
3667
3676
|
|
|
@@ -3706,8 +3715,8 @@ async function normalizedFactTerm(term) {
|
|
|
3706
3715
|
* "store"; "every store is a container" then needs "store" to read as known
|
|
3707
3716
|
* even though it's not in the static lexicon at all).
|
|
3708
3717
|
*
|
|
3709
|
-
* This is the narrowest of three isa-anchor tiers:
|
|
3710
|
-
* (right below) anchors on
|
|
3718
|
+
* This is the narrowest of three isa-anchor tiers: isAnchoredTerm
|
|
3719
|
+
* (right below) anchors on an anchor-band isa row instead of an
|
|
3711
3720
|
* operator-taught one; isGroundedTerm (below that) folds in the static
|
|
3712
3721
|
* lexicon and GENERIC_ANCHOR_NOUNS too. A caller that specifically needs
|
|
3713
3722
|
* "the operator SAID this" — a proof chain citing its own warrant, a
|
|
@@ -3722,17 +3731,17 @@ async function isGroundedByFact(term, memoryDir, cache = null) {
|
|
|
3722
3731
|
return (await isaTermIndex(memoryDir, cache)).taught.has(t);
|
|
3723
3732
|
}
|
|
3724
3733
|
|
|
3725
|
-
/**
|
|
3726
|
-
*
|
|
3727
|
-
* (
|
|
3734
|
+
/** ANCHORED tier of the isa-anchor ladder: true when `term` is the subject or
|
|
3735
|
+
* object of an isa-family fact whose source is the anchor band
|
|
3736
|
+
* (ANCHOR_SOURCE_TYPES) — a reference source's own claim that the term
|
|
3728
3737
|
* denotes a class, not necessarily anything the operator taught. See
|
|
3729
3738
|
* isAnchorableTerm, just below isGroundedTerm, for where this tier and the
|
|
3730
3739
|
* taught tier combine into "anchorable in any sense". */
|
|
3731
|
-
async function
|
|
3740
|
+
async function isAnchoredTerm(term, memoryDir, cache = null) {
|
|
3732
3741
|
if (!memoryDir) return false;
|
|
3733
3742
|
const t = await normalizedFactTerm(term);
|
|
3734
3743
|
if (!t) return false;
|
|
3735
|
-
return (await isaTermIndex(memoryDir, cache)).
|
|
3744
|
+
return (await isaTermIndex(memoryDir, cache)).anchored.has(t);
|
|
3736
3745
|
}
|
|
3737
3746
|
|
|
3738
3747
|
/** A bare single alphabetic character ("a", "i", …) classify() resolves only
|
|
@@ -3772,14 +3781,14 @@ export { isGroundedTerm };
|
|
|
3772
3781
|
|
|
3773
3782
|
/** "Anchorable in ANY sense" — everything isGroundedTerm already tests
|
|
3774
3783
|
* (static lexicon, GENERIC_ANCHOR_NOUNS, a resolved code-graph symbol, a
|
|
3775
|
-
* prior-taught isa fact) PLUS the
|
|
3776
|
-
*
|
|
3777
|
-
*
|
|
3778
|
-
*
|
|
3779
|
-
*
|
|
3784
|
+
* prior-taught isa fact) PLUS the anchored tier (isAnchoredTerm, above): a
|
|
3785
|
+
* term an anchor-band source's own isa row already names as a class.
|
|
3786
|
+
* isGroundedTerm itself stays exactly as narrow as before (it's exported and
|
|
3787
|
+
* pinned by test/services/chat-grounding.test.mjs) — every widening onto the
|
|
3788
|
+
* anchor band goes through this function instead. */
|
|
3780
3789
|
async function isAnchorableTerm(term, lex, memoryDir, cache = null, graph = null) {
|
|
3781
3790
|
if (await isGroundedTerm(term, lex, memoryDir, cache, graph)) return true;
|
|
3782
|
-
return
|
|
3791
|
+
return isAnchoredTerm(term, memoryDir, cache);
|
|
3783
3792
|
}
|
|
3784
3793
|
export { isAnchorableTerm };
|
|
3785
3794
|
|
|
@@ -4223,17 +4232,17 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon,
|
|
|
4223
4232
|
&& !/\s/.test(subjectRaw)
|
|
4224
4233
|
&& objectCarriesArticle(payload)
|
|
4225
4234
|
&& readsAsIndividualName(subjectRaw, lex);
|
|
4226
|
-
// A PRIOR turn's minted term, a GENERIC_ANCHOR_NOUNS root, or
|
|
4227
|
-
//
|
|
4228
|
-
// lexicon noun — all three are always treated as class-level (never
|
|
4235
|
+
// A PRIOR turn's minted term, a GENERIC_ANCHOR_NOUNS root, or an
|
|
4236
|
+
// anchor-band source's own isa row grounds Y just as legitimately as a
|
|
4237
|
+
// static lexicon noun — all three are always treated as class-level (never
|
|
4229
4238
|
// property), consistent with unknownObjectFallback (below) always minting
|
|
4230
|
-
// a CLASS. Without the
|
|
4231
|
-
//
|
|
4232
|
-
//
|
|
4233
|
-
//
|
|
4239
|
+
// a CLASS. Without the anchor check, "p is a kind of consonant" (both sides
|
|
4240
|
+
// anchored, neither in the static lexicon) refuses; with it, "florb is a
|
|
4241
|
+
// kind of glyph" also stores, minting "florb" — one anchored side is
|
|
4242
|
+
// enough, the same rule the taught tier already applies.
|
|
4234
4243
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
4235
4244
|
|| (await isGroundedByFact(objectRaw, memoryDir, cache))
|
|
4236
|
-
|| (await
|
|
4245
|
+
|| (await isAnchoredTerm(objectRaw, memoryDir, cache))) {
|
|
4237
4246
|
return teachFact(memoryDir, sessionId, {
|
|
4238
4247
|
subject,
|
|
4239
4248
|
predicate: namedIndividual ? TYPE_PREDICATE : SUBCLASS_PREDICATE,
|
|
@@ -4338,9 +4347,9 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon, c
|
|
|
4338
4347
|
if (!/^(?:every|each|all|any)$/i.test((det || "").trim()) && !classIntent) return null; // class-level mint needs a universal quantifier or an explicit kind-of infix
|
|
4339
4348
|
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
4340
4349
|
const lex = lexicon || loadLexicon();
|
|
4341
|
-
// Anchorable (not just lexicon-grounded):
|
|
4342
|
-
//
|
|
4343
|
-
//
|
|
4350
|
+
// Anchorable (not just lexicon-grounded): an anchored subject earns the
|
|
4351
|
+
// same mint a lexicon-grounded one does ("p is a kind of alphabet letter"
|
|
4352
|
+
// — "p" anchored via a corpus isa row, "alphabet letter" minted).
|
|
4344
4353
|
const subjectGrounded = await isAnchorableTerm(subjectRaw, lex, memoryDir, cache, graph);
|
|
4345
4354
|
if (!subjectGrounded) return null; // ungrounded subject isn't this fallback's asymmetry — never a guessed mint
|
|
4346
4355
|
// The OBJECT gate stays on isGroundedTerm, deliberately narrower than the
|
|
@@ -4449,15 +4458,15 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
4449
4458
|
if (/\s+(?:is|are)\s+an?\s+[\w-]+[.!?]*\s*$/i.test(String(payload).trim())) return null;
|
|
4450
4459
|
const { loadLexicon, lookupNoun, lookupAdjective, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
4451
4460
|
const lex = lexicon || loadLexicon();
|
|
4452
|
-
// Y already a known NOUN, a fact-grounded CLASS term, or
|
|
4453
|
-
//
|
|
4461
|
+
// Y already a known NOUN, a fact-grounded CLASS term, or an anchored CLASS
|
|
4462
|
+
// term — a genuine class-membership sentence, unknownSubjectFallback/
|
|
4454
4463
|
// unknownObjectFallback's own territory (already had first refusal on it)
|
|
4455
|
-
// — never misread as a property. The
|
|
4456
|
-
//
|
|
4457
|
-
//
|
|
4464
|
+
// — never misread as a property. The anchor check is required here, not
|
|
4465
|
+
// optional: without it an anchored object could be misread as a brand-new
|
|
4466
|
+
// adjective instead of the class term it is.
|
|
4458
4467
|
if (lookupNoun(lex, objectRaw) || GENERIC_ANCHOR_NOUNS.has(String(objectRaw).toLowerCase())
|
|
4459
4468
|
|| (await isGroundedByFact(objectRaw, memoryDir, cache))
|
|
4460
|
-
|| (await
|
|
4469
|
+
|| (await isAnchoredTerm(objectRaw, memoryDir, cache))) return null;
|
|
4461
4470
|
// CLASS-LEVEL adjective predication — "every snake is venomous": a universal
|
|
4462
4471
|
// quantifier over a grounded noun class, with an adjective complement. The
|
|
4463
4472
|
// quantifier is the same deliberate-generalization signal the article/
|
|
@@ -4472,7 +4481,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
4472
4481
|
// itself carries the safety here, so leaving this narrow would make
|
|
4473
4482
|
// "every p is a glyph" teach while "every p is venomous" refuses, an
|
|
4474
4483
|
// unpredictable asymmetry between two universally-quantified sentences
|
|
4475
|
-
// over the same
|
|
4484
|
+
// over the same anchored subject.
|
|
4476
4485
|
// A bare GENERIC PLURAL subject with no determiner at all ("animals are
|
|
4477
4486
|
// alive") carries the same implicit-universal reading matchBareHabitualTeach
|
|
4478
4487
|
// already gives a bare plural verb subject ("dogs bark" mints the same
|
|
@@ -4502,13 +4511,13 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
4502
4511
|
const bareSubject = subjectRaw.replace(/^(?:the|an?)\s+/i, "").trim() || subjectRaw;
|
|
4503
4512
|
const hadArticle = bareSubject !== subjectRaw;
|
|
4504
4513
|
const capitalized = /^[A-Z]/.test(bareSubject);
|
|
4505
|
-
// Deliberately isGroundedByFact (taught-only), NOT
|
|
4514
|
+
// Deliberately isGroundedByFact (taught-only), NOT isAnchoredTerm or
|
|
4506
4515
|
// isAnchorableTerm: this is the bare, unquantified property claim, where
|
|
4507
4516
|
// NOTHING else stands in for the article/capitalization/quantifier
|
|
4508
|
-
// "deliberate entity" signal this function's own docblock requires.
|
|
4509
|
-
//
|
|
4517
|
+
// "deliberate entity" signal this function's own docblock requires.
|
|
4518
|
+
// Anchoring a bare subject here would reopen the pinned "module is banana"
|
|
4510
4519
|
// regression under a new spelling — "dog is banana" would silently mint
|
|
4511
|
-
// dog mgx:hasProperty banana, since "dog" is
|
|
4520
|
+
// dog mgx:hasProperty banana, since "dog" is anchored via ConceptNet.
|
|
4512
4521
|
const factGrounded = await isGroundedByFact(bareSubject, memoryDir, cache);
|
|
4513
4522
|
const genericAnchor = GENERIC_ANCHOR_NOUNS.has(bareSubject.toLowerCase());
|
|
4514
4523
|
// A bare (no article, no capitalization) subject grounded ONLY via the
|
|
@@ -7147,8 +7156,8 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
7147
7156
|
} catch { /* lexicon unavailable — fall through to the generic message */ }
|
|
7148
7157
|
}
|
|
7149
7158
|
// Residue is what the STATIC lexicon didn't recognize. A token the fact
|
|
7150
|
-
// store anchors — taught,
|
|
7151
|
-
// does know, so it must not be named here. Only a token unknown under
|
|
7159
|
+
// store anchors — taught, anchor-band, or a code-graph symbol — is a word
|
|
7160
|
+
// tmct does know, so it must not be named here. Only a token unknown under
|
|
7152
7161
|
// EVERY tier is. Deliberately not isAnchorableTerm: its classify branch
|
|
7153
7162
|
// would drop a token the lexicon knows under the WRONG part of speech, and
|
|
7154
7163
|
// that token genuinely IS the problem — this loop stays word-anchor-only.
|
|
@@ -7157,7 +7166,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
7157
7166
|
const survivors = [];
|
|
7158
7167
|
for (const w of unknown) {
|
|
7159
7168
|
if (await isGroundedByFact(w, memoryDir, cache)) continue;
|
|
7160
|
-
if (await
|
|
7169
|
+
if (await isAnchoredTerm(w, memoryDir, cache)) continue;
|
|
7161
7170
|
if (graph && resolveSymbol(graph, w)?.match) continue;
|
|
7162
7171
|
survivors.push(w);
|
|
7163
7172
|
}
|
|
@@ -8147,9 +8156,10 @@ function citationProvenance(provenance) {
|
|
|
8147
8156
|
}
|
|
8148
8157
|
|
|
8149
8158
|
/** What every rendered fact line ends with: the extractor's caveat, when the
|
|
8150
|
-
* row carries a finding worth declaring, then the
|
|
8151
|
-
*
|
|
8152
|
-
*
|
|
8159
|
+
* row carries a finding worth declaring, then the speaker a report attributed
|
|
8160
|
+
* the claim to, then the source citation. The order is fixed — a reader sees
|
|
8161
|
+
* how the sentence was read, then who said it, then where it came from — and
|
|
8162
|
+
* every part is optional, so a clean untraced row ends with nothing at all.
|
|
8153
8163
|
*
|
|
8154
8164
|
* A row whose extractor recorded a finding stays answerable (no lane declines
|
|
8155
8165
|
* a stored fact); the caveat is how the answer says which reading it leans
|
|
@@ -8158,7 +8168,24 @@ function citationProvenance(provenance) {
|
|
|
8158
8168
|
function factLineTail(f) {
|
|
8159
8169
|
const caveat = findingCaveat(f);
|
|
8160
8170
|
const cite = f.provenance ? ` (source: ${citationProvenance(f.provenance)})` : "";
|
|
8161
|
-
return `${caveat ? ` ${caveat}` : ""}${cite}`;
|
|
8171
|
+
return `${caveat ? ` ${caveat}` : ""}${attributionClause(f)}${cite}`;
|
|
8172
|
+
}
|
|
8173
|
+
|
|
8174
|
+
/** "(president trump said)" — the speaker a report attributed this claim to,
|
|
8175
|
+
* off the `attributedTo` the fold hangs on an attributed row. A claim stays
|
|
8176
|
+
* grounded and answerable either way; naming the speaker is what keeps the
|
|
8177
|
+
* answer from reading as tmct's own assertion.
|
|
8178
|
+
*
|
|
8179
|
+
* Two speakers on one claim both get named: two outlets attributing one claim
|
|
8180
|
+
* to two people corroborate it, they do not disagree. Sorted and deduped, so
|
|
8181
|
+
* the line reads the same however the rows arrived. */
|
|
8182
|
+
function attributionClause(f) {
|
|
8183
|
+
const speakers = [...new Set((Array.isArray(f?.attributedTo) ? f.attributedTo : []).filter(Boolean))].sort();
|
|
8184
|
+
if (!speakers.length) return "";
|
|
8185
|
+
const named = speakers.length === 1
|
|
8186
|
+
? speakers[0]
|
|
8187
|
+
: `${speakers.slice(0, -1).join(", ")} and ${speakers[speakers.length - 1]}`;
|
|
8188
|
+
return ` (${named} said)`;
|
|
8162
8189
|
}
|
|
8163
8190
|
|
|
8164
8191
|
/** Every extraction finding the rows behind ONE composed sentence carry. A
|
|
@@ -8168,6 +8195,14 @@ function composedExtraction(rows) {
|
|
|
8168
8195
|
return rows.filter(Boolean).flatMap((r) => (Array.isArray(r.extraction) ? r.extraction : []));
|
|
8169
8196
|
}
|
|
8170
8197
|
|
|
8198
|
+
/** Every speaker the rows behind ONE composed sentence were attributed to,
|
|
8199
|
+
* gathered the same way and for the same reason as the findings above: a
|
|
8200
|
+
* surface that cannot name the speaker must not print the claim, and a
|
|
8201
|
+
* composed sentence prints several rows at once. */
|
|
8202
|
+
function composedAttribution(rows) {
|
|
8203
|
+
return rows.filter(Boolean).flatMap((r) => (Array.isArray(r.attributedTo) ? r.attributedTo : []));
|
|
8204
|
+
}
|
|
8205
|
+
|
|
8171
8206
|
/** One fact as an uncited-framing citation step: the phrase, its caveat and
|
|
8172
8207
|
* its source. Chains, both-sides refusals and "via:" receipts all read a
|
|
8173
8208
|
* premise this way — the same convention renderFactLine uses, without the
|
|
@@ -8215,14 +8250,20 @@ function renderFactLine(f) {
|
|
|
8215
8250
|
* row-per-member dump: "every pet is a cat or a dog (source: …)". `node` is
|
|
8216
8251
|
* the `<parent> rdfs:subClassOf <unionNode>` fact row (the parent and the
|
|
8217
8252
|
* citation both come from it); `members` are the union node's own
|
|
8218
|
-
* `owl:unionOf` rows, sorted here by
|
|
8219
|
-
* reads back the same way regardless of the order its rows arrived in
|
|
8253
|
+
* `owl:unionOf` rows, sorted here by content so the same union always
|
|
8254
|
+
* reads back the same way regardless of the order its rows arrived in, and
|
|
8255
|
+
* regardless of the reader's locale. Every member shares the union node's
|
|
8256
|
+
* subject and predicate, so content order is member name then source.
|
|
8220
8257
|
* Pure — takes the fact rows, returns one line. */
|
|
8221
8258
|
function renderUnionLine(node, members) {
|
|
8222
|
-
const sorted = [...members].sort(
|
|
8259
|
+
const sorted = [...members].sort(compareFactsByContent);
|
|
8223
8260
|
// One sentence states every arm, so its caveat covers every row it was
|
|
8224
8261
|
// composed from, not just the one the citation came off.
|
|
8225
|
-
const tail = factLineTail({
|
|
8262
|
+
const tail = factLineTail({
|
|
8263
|
+
...node,
|
|
8264
|
+
extraction: composedExtraction([node, ...sorted]),
|
|
8265
|
+
attributedTo: composedAttribution([node, ...sorted]),
|
|
8266
|
+
});
|
|
8226
8267
|
const arms = sorted.map((m) => `${indefiniteArticleFor(m.object)} ${m.object}`).join(" or ");
|
|
8227
8268
|
return `every ${node.subject} is ${arms}${tail}`;
|
|
8228
8269
|
}
|
|
@@ -8230,19 +8271,25 @@ function renderUnionLine(node, members) {
|
|
|
8230
8271
|
/** An enumerated class's stored `owl:oneOf` triples read back as ONE sentence:
|
|
8231
8272
|
* "the primary colours are exactly red, yellow and blue (source: …)". `cls`
|
|
8232
8273
|
* is the enumerated class's own term; `members` are its `owl:oneOf` rows,
|
|
8233
|
-
* sorted here by
|
|
8234
|
-
* keeps
|
|
8274
|
+
* sorted here by content for the same order- and locale-independence
|
|
8275
|
+
* renderUnionLine keeps; the source the line cites is read off the first
|
|
8276
|
+
* member in that order, so it settles the same way for every reader.
|
|
8277
|
+
* The class name pluralizes through the same naive "+s/+es/+ies" fold
|
|
8235
8278
|
* thirdPersonSingularSurface already applies to a verb lemma — the same
|
|
8236
8279
|
* accepted trade documented there. Pure — takes the fact rows, returns one
|
|
8237
8280
|
* line. */
|
|
8238
8281
|
function renderEnumerationLine(cls, members) {
|
|
8239
|
-
const sorted = [...members].sort(
|
|
8282
|
+
const sorted = [...members].sort(compareFactsByContent);
|
|
8240
8283
|
const names = sorted.map((m) => m.object);
|
|
8241
8284
|
const list = names.length > 1
|
|
8242
8285
|
? `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`
|
|
8243
8286
|
: names.join("");
|
|
8244
8287
|
const citeSource = sorted.find((m) => m.provenance)?.provenance;
|
|
8245
|
-
const tail = factLineTail({
|
|
8288
|
+
const tail = factLineTail({
|
|
8289
|
+
provenance: citeSource || "",
|
|
8290
|
+
extraction: composedExtraction(sorted),
|
|
8291
|
+
attributedTo: composedAttribution(sorted),
|
|
8292
|
+
});
|
|
8246
8293
|
const plural = thirdPersonSingularSurface(String(cls || "").replace(/-/g, " "));
|
|
8247
8294
|
return `the ${plural} are exactly ${list}${tail}`;
|
|
8248
8295
|
}
|
|
@@ -8258,12 +8305,57 @@ function renderEnumerationLine(cls, members) {
|
|
|
8258
8305
|
function renderUniversalRestrictionLine(node, rows) {
|
|
8259
8306
|
const onProperty = rows.find((r) => r.predicate === "owl:onProperty");
|
|
8260
8307
|
const allValuesFrom = rows.find((r) => r.predicate === "owl:allValuesFrom");
|
|
8261
|
-
const tail = factLineTail({
|
|
8308
|
+
const tail = factLineTail({
|
|
8309
|
+
...node,
|
|
8310
|
+
extraction: composedExtraction([node, onProperty, allValuesFrom]),
|
|
8311
|
+
attributedTo: composedAttribution([node, onProperty, allValuesFrom]),
|
|
8312
|
+
});
|
|
8262
8313
|
const filler = thirdPersonSingularSurface(String(allValuesFrom?.object || "").replace(/-/g, " "));
|
|
8263
8314
|
return `every ${node.subject} ${onProperty?.object || ""} only ${filler}${tail}`;
|
|
8264
8315
|
}
|
|
8265
8316
|
export { renderUnionLine, renderEnumerationLine, renderUniversalRestrictionLine };
|
|
8266
8317
|
|
|
8318
|
+
/** The read-time twin of sense-gate.mjs's screen on isa DERIVATION. The
|
|
8319
|
+
* WordNet-derived bands flatten every sense of a word onto one label —
|
|
8320
|
+
* "region" carries a geographic sense and an anatomical one, and both rows
|
|
8321
|
+
* store the same six characters — so a walk that climbs through the shared
|
|
8322
|
+
* label comes straight back down the other sense: russia ⊑ country ⊑
|
|
8323
|
+
* geographical area ⊑ region ⊑ body part. The offline closure already
|
|
8324
|
+
* refuses that step. The readers here walk the same edges live, one question
|
|
8325
|
+
* at a time, and so need the same screen.
|
|
8326
|
+
*
|
|
8327
|
+
* Built over the ASSERTED isa rows only. An entailed row is the closure's
|
|
8328
|
+
* own output, and feeding that back in is what the gate exists to stop. A
|
|
8329
|
+
* stated row is never screened either: nothing here constrains what a corpus
|
|
8330
|
+
* or a teacher recorded, only what a multi-hop walk concludes on top of it.
|
|
8331
|
+
*
|
|
8332
|
+
* Pure over the fact set — buildSenseGate sorts every frontier and memoizes
|
|
8333
|
+
* per term, so two ingestion orders of the same rows give the same verdicts.
|
|
8334
|
+
* Held on the caller's per-turn factRows cache and keyed on the row array
|
|
8335
|
+
* itself, so one turn builds one gate however many readers ask for it. */
|
|
8336
|
+
function readTimeSenseScreen(rows, cache = null) {
|
|
8337
|
+
if (cache?.senseScreen?.rows === rows) return cache.senseScreen.gate;
|
|
8338
|
+
const subClassEdges = [];
|
|
8339
|
+
const typeEdges = [];
|
|
8340
|
+
for (const f of rows || []) {
|
|
8341
|
+
if (String(f.provenance || "").includes("entailed:")) continue;
|
|
8342
|
+
if (f.predicate === SUBCLASS_PREDICATE) subClassEdges.push([f.subject, f.object]);
|
|
8343
|
+
else if (f.predicate === TYPE_PREDICATE) typeEdges.push([f.subject, f.object]);
|
|
8344
|
+
}
|
|
8345
|
+
const gate = buildSenseGate({ subClassEdges, typeEdges });
|
|
8346
|
+
if (cache) cache.senseScreen = { rows, gate };
|
|
8347
|
+
return gate;
|
|
8348
|
+
}
|
|
8349
|
+
|
|
8350
|
+
/** True when concluding `from ⊑ <the asked term>` would cross two senses the
|
|
8351
|
+
* screen holds apart. Every spelling variant of the asked term is tried: an
|
|
8352
|
+
* unplaced spelling resolves to no top and so blocks nothing, which is the
|
|
8353
|
+
* gate's own "one unresolved end lets it through" rule. */
|
|
8354
|
+
function senseScreenBlocksWalk(screen, from, toVariants) {
|
|
8355
|
+
for (const to of toVariants) if (screen.declines(from, to)) return true;
|
|
8356
|
+
return false;
|
|
8357
|
+
}
|
|
8358
|
+
|
|
8267
8359
|
/** Append an is-a object's superclass chain to its rendered fact line, before
|
|
8268
8360
|
* the caveat and the citation: "rover is a kind of dog" becomes "rover is a
|
|
8269
8361
|
* kind of dog → canine → mammal → animal". Only the subject-side is-a lines
|
|
@@ -8271,11 +8363,20 @@ export { renderUnionLine, renderEnumerationLine, renderUniversalRestrictionLine
|
|
|
8271
8363
|
*
|
|
8272
8364
|
* `toward` steers the chain toward a specific ancestor (the class a "list …"
|
|
8273
8365
|
* question actually asked about) when the is-a object has more than one
|
|
8274
|
-
* taught parent.
|
|
8275
|
-
|
|
8366
|
+
* taught parent.
|
|
8367
|
+
*
|
|
8368
|
+
* `screen` (readTimeSenseScreen's gate) trims the chain where it would cross
|
|
8369
|
+
* a sense join, so the rendered line stops at the last ancestor the fact's
|
|
8370
|
+
* own object really sits under rather than reading "…→ region → body part"
|
|
8371
|
+
* off a geographic subject. */
|
|
8372
|
+
function renderFactLineWithChain(f, parents, subjectVariants, { toward = null, screen = null } = {}) {
|
|
8276
8373
|
const base = renderFactLine(f);
|
|
8277
8374
|
if (!ISA_PREDICATES.has(f.predicate) || !subjectVariants.has(f.subject)) return base;
|
|
8278
|
-
|
|
8375
|
+
let chain = ancestryChain(f.object, parents, { cap: 6, stopAt: ANSWER_STOP_SET, toward });
|
|
8376
|
+
if (screen) {
|
|
8377
|
+
const crossing = chain.findIndex((node, i) => i > 0 && screen.declines(f.object, node));
|
|
8378
|
+
if (crossing > 0) chain = chain.slice(0, crossing);
|
|
8379
|
+
}
|
|
8279
8380
|
if (chain.length <= 1) return base;
|
|
8280
8381
|
const suffix = ` → ${chain.slice(1).join(" → ")}`;
|
|
8281
8382
|
const tail = factLineTail(f);
|
|
@@ -8292,10 +8393,11 @@ function renderFactLineWithChain(f, parents, subjectVariants, { toward = null }
|
|
|
8292
8393
|
* Returns `{ lines, grouped }`. `lines` is the flat, chain-enhanced rendering
|
|
8293
8394
|
* (indented by `indent`) the caller uses when senses do not split. `grouped`
|
|
8294
8395
|
* is a ready `{ text, replace, pending? }` answer when they do, else null. */
|
|
8295
|
-
function senseSplitFactList(hits, rows, subjectVariants, { indent = "" } = {}) {
|
|
8396
|
+
function senseSplitFactList(hits, rows, subjectVariants, { indent = "", cache = null } = {}) {
|
|
8296
8397
|
const subClassEdges = rows.filter((f) => f.predicate === SUBCLASS_PREDICATE).map((f) => [f.subject, f.object]);
|
|
8297
8398
|
const parents = subClassParents(subClassEdges);
|
|
8298
|
-
const
|
|
8399
|
+
const screen = readTimeSenseScreen(rows, cache);
|
|
8400
|
+
const lines = hits.map((f) => `${indent}${renderFactLineWithChain(f, parents, subjectVariants, { screen })}`);
|
|
8299
8401
|
|
|
8300
8402
|
const isaSubjectFacts = hits.filter((f) => ISA_PREDICATES.has(f.predicate) && subjectVariants.has(f.subject));
|
|
8301
8403
|
const isaObjects = [...new Set(isaSubjectFacts.map((f) => f.object))];
|
|
@@ -8313,7 +8415,7 @@ function senseSplitFactList(hits, rows, subjectVariants, { indent = "" } = {}) {
|
|
|
8313
8415
|
const restItems = [];
|
|
8314
8416
|
let shownCount = 0;
|
|
8315
8417
|
const addLine = (f) => {
|
|
8316
|
-
const rendered = renderFactLineWithChain(f, parents, subjectVariants);
|
|
8418
|
+
const rendered = renderFactLineWithChain(f, parents, subjectVariants, { screen });
|
|
8317
8419
|
if (shownCount < FACT_ANSWER_CAP) { shownCount += 1; return `${indent}${rendered}`; }
|
|
8318
8420
|
restItems.push(rendered);
|
|
8319
8421
|
return null;
|
|
@@ -8786,6 +8888,14 @@ const RECURSIVE_LIST_ASK_RE = /^list\s+(?:the\s+|all\s+)?([a-z][\w-]*)\s+of\s+([
|
|
|
8786
8888
|
const ISA_ASK_RE = /^(?:is|are)\s+(?:an?\s+)?(.+?)\s+(?:a\s+kind\s+of|a\s+type\s+of|an?)\s+(.+?)[?.!\s]*$/i;
|
|
8787
8889
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
8788
8890
|
|
|
8891
|
+
/** An already-ranked hit list with the subject's type facts moved to the
|
|
8892
|
+
* front, each half keeping its ranked order. What a thing IS answers "what is
|
|
8893
|
+
* X" more directly than anything else the store holds about it. */
|
|
8894
|
+
const definitionFirst = (hits) => [
|
|
8895
|
+
...hits.filter((f) => ISA_PREDICATES.has(f.predicate)),
|
|
8896
|
+
...hits.filter((f) => !ISA_PREDICATES.has(f.predicate)),
|
|
8897
|
+
];
|
|
8898
|
+
|
|
8789
8899
|
/** How far the isa ladder's miss text probes for a chain it can name a
|
|
8790
8900
|
* recovery for. Purely a REPORTING reach: the live chases answer within their
|
|
8791
8901
|
* own hop bounds and this never widens them, it only tells the miss whether
|
|
@@ -9434,9 +9544,12 @@ async function restrictionExistentialHit(memoryDir, cache, subjectVariants, fill
|
|
|
9434
9544
|
// the ones meant to answer, including a provable "no".
|
|
9435
9545
|
const subjectRestrictions = rows.filter((r) => r.predicate === SUBCLASS_PREDICATE && subjectVariants.has(r.subject)
|
|
9436
9546
|
&& onPropertyOf.has(r.object) && someValuesFromOf.has(r.object));
|
|
9547
|
+
// Which restriction gets cited has to settle the same way for every reader,
|
|
9548
|
+
// so the pick runs on the restriction node's id in codepoint order and falls
|
|
9549
|
+
// through to the row's own content when two subject variants point at one node.
|
|
9437
9550
|
const hit = subjectRestrictions
|
|
9438
9551
|
.filter((r) => fillerVariants.has(someValuesFromOf.get(r.object)))
|
|
9439
|
-
.sort((a, b) => a.object
|
|
9552
|
+
.sort((a, b) => byCodepoint(a.object, b.object) || compareFactsByContent(a, b))[0];
|
|
9440
9553
|
if (hit) {
|
|
9441
9554
|
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
9442
9555
|
const premises = (hit.justification || [])
|
|
@@ -9853,7 +9966,7 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
9853
9966
|
const containerVariants = factTermVariants(normFactTerm, containerRaw.replace(/^(?:an?|the)\s+/i, "").trim());
|
|
9854
9967
|
const hits = (await factRows(memoryDir, cache)).filter(
|
|
9855
9968
|
(f) => predicates.includes(f.predicate) && containerVariants.has(f.object),
|
|
9856
|
-
)
|
|
9969
|
+
);
|
|
9857
9970
|
if (hits.length) {
|
|
9858
9971
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
9859
9972
|
const lines = ranked.map(renderFactLine);
|
|
@@ -10049,10 +10162,15 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
10049
10162
|
}
|
|
10050
10163
|
// Bias only REORDERS — every hit still renders and is cited (Part 6's
|
|
10051
10164
|
// "disclosed, never dropped" contract). Unconfigured/tied bias degrades to
|
|
10052
|
-
// trust-desc
|
|
10053
|
-
|
|
10165
|
+
// trust-desc.
|
|
10166
|
+
//
|
|
10167
|
+
// A definition then leads with what the subject IS. Bias and trust say
|
|
10168
|
+
// nothing about that, and once they tie the isa row sorts wherever its
|
|
10169
|
+
// predicate spelling happens to fall, so "what is a dog" opens on "dog can
|
|
10170
|
+
// bark". Every other hit keeps its ranked place behind it.
|
|
10171
|
+
hits = definitionFirst(rankByBiasThenTrust(hits, biasByBundle));
|
|
10054
10172
|
const allRows = await factRows(memoryDir, cache);
|
|
10055
|
-
const { lines, grouped } = senseSplitFactList(hits, allRows, variants);
|
|
10173
|
+
const { lines, grouped } = senseSplitFactList(hits, allRows, variants, { cache });
|
|
10056
10174
|
// A long undifferentiated "what is X" leads with the digest — a bounded
|
|
10057
10175
|
// narrative over the same facts — and holds the full list behind the escape.
|
|
10058
10176
|
// It wins over the sense-split grouping here: the digest's own selector
|
|
@@ -10657,12 +10775,21 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
10657
10775
|
// framing on, just above.
|
|
10658
10776
|
const isTaughtFact = (f) => !String(f.provenance || "").includes("corpus:") && !String(f.provenance || "").includes("web:");
|
|
10659
10777
|
const isaRows = rows.filter((f) => ISA_PREDICATES.has(f.predicate) && isTaughtFact(f));
|
|
10778
|
+
// Each subject this walk collects is a claim that it is a subtype of the
|
|
10779
|
+
// ASKED term — a conclusion the walk derives, never a row anyone stated —
|
|
10780
|
+
// so it takes the same disjointness screen the offline closure applies to
|
|
10781
|
+
// the same conclusion. Without it a sense-mixed label part-way up the
|
|
10782
|
+
// chain carries the walk down the other sense, and "what do you know
|
|
10783
|
+
// about body part" answers with facts about Russia.
|
|
10784
|
+
const subtypeScreen = readTimeSenseScreen(rows, cache);
|
|
10660
10785
|
const subtypeSubjects = new Set();
|
|
10661
10786
|
let frontier = variants;
|
|
10662
10787
|
for (let hop = 0; hop < 8 && frontier.size; hop += 1) {
|
|
10663
10788
|
const nextSubjects = new Set();
|
|
10664
10789
|
for (const f of isaRows) {
|
|
10665
|
-
if (frontier.has(f.object)
|
|
10790
|
+
if (!frontier.has(f.object) || subtypeSubjects.has(f.subject)) continue;
|
|
10791
|
+
if (senseScreenBlocksWalk(subtypeScreen, f.subject, variants)) continue;
|
|
10792
|
+
nextSubjects.add(f.subject);
|
|
10666
10793
|
}
|
|
10667
10794
|
if (!nextSubjects.size) break;
|
|
10668
10795
|
for (const s of nextSubjects) subtypeSubjects.add(s);
|
|
@@ -10761,7 +10888,7 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
10761
10888
|
hits = rankByBiasThenTrust(hits, biasByBundle);
|
|
10762
10889
|
const header = `${hits.length} remembered fact${hits.length === 1 ? "" : "s"} about ${term}`
|
|
10763
10890
|
+ `${viaSubtype ? " (including its known subtypes)" : ""}:`;
|
|
10764
|
-
const { lines, grouped } = senseSplitFactList(hits, rows, variants, { indent: " " });
|
|
10891
|
+
const { lines, grouped } = senseSplitFactList(hits, rows, variants, { indent: " ", cache });
|
|
10765
10892
|
if (grouped) return { ...grouped, text: `${header}\n${grouped.text}`, ...(weakOnly ? { weakOnly: true } : {}) };
|
|
10766
10893
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
10767
10894
|
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
@@ -10845,7 +10972,7 @@ async function whatElseAnswer(memoryDir, query, last) {
|
|
|
10845
10972
|
let normFactTerm;
|
|
10846
10973
|
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
10847
10974
|
const variants = factTermVariants(normFactTerm, term);
|
|
10848
|
-
const hits = (await memoryFacts(memoryDir)).filter((f) => variants.has(f.subject));
|
|
10975
|
+
const hits = (await memoryFacts(memoryDir)).filter((f) => variants.has(f.subject)).sort(compareFactsByContent);
|
|
10849
10976
|
const picture = pickPhrase("full-picture", term.toLowerCase(), "the full picture");
|
|
10850
10977
|
const nothingMore = {
|
|
10851
10978
|
text: `That's everything I know about "${term}" — /memory to see ${picture}.`,
|
|
@@ -10888,7 +11015,7 @@ async function synonymFactAnswer(memoryDir, query, envelope) {
|
|
|
10888
11015
|
const facts = await memoryFacts(memoryDir);
|
|
10889
11016
|
for (const { variant, source } of await synonymsOf(term)) {
|
|
10890
11017
|
const variants = factTermVariants(normFactTerm, variant);
|
|
10891
|
-
const hits = facts.filter((f) => variants.has(f.subject));
|
|
11018
|
+
const hits = facts.filter((f) => variants.has(f.subject)).sort(compareFactsByContent);
|
|
10892
11019
|
if (!hits.length) continue;
|
|
10893
11020
|
const lines = hits.map(renderFactLine);
|
|
10894
11021
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
@@ -11263,7 +11390,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
11263
11390
|
if (!((fallThroughIsa && !/^there\b/i.test(fallThroughIsa[1].trim())) || CONFIRM_TAG_RE.test(q))) return null;
|
|
11264
11391
|
}
|
|
11265
11392
|
const isa = rows.filter((f) => ISA_PREDICATES.has(f.predicate));
|
|
11266
|
-
const byTrust = (a, b) => b.trust - a.trust;
|
|
11393
|
+
const byTrust = (a, b) => (b.trust - a.trust) || compareFactsByContent(a, b);
|
|
11267
11394
|
const renderMany = (hits) => {
|
|
11268
11395
|
const lines = hits.map(renderFactLine);
|
|
11269
11396
|
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
@@ -11812,9 +11939,17 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
11812
11939
|
// technically-true-per-ConceptNet "yes" that has nothing to do with
|
|
11813
11940
|
// what the OPERATOR taught; only operator/teach/entailed-sourced isa
|
|
11814
11941
|
// facts are chased, matching "TAUGHT" in the gap's own name.
|
|
11942
|
+
// Every chase from here down concludes a subsumption nobody stated, so
|
|
11943
|
+
// each takes the same disjointness screen the offline closure applies to
|
|
11944
|
+
// the same conclusion: a subject whose own asserted chain places it under
|
|
11945
|
+
// a top disjoint from the asked class is not chased at all. A stated fact
|
|
11946
|
+
// is untouched — the direct-hit and polarity branches above have already
|
|
11947
|
+
// returned by the time this runs, so the screen only ever costs a walk.
|
|
11948
|
+
const chaseSenseScreen = readTimeSenseScreen(rows, cache);
|
|
11949
|
+
const chaseSubjects = [...subjCandidates].filter((s) => !senseScreenBlocksWalk(chaseSenseScreen, s, objVariants));
|
|
11815
11950
|
const factForStep = (step) => (step.predicate === SC_PREDICATE ? chainSubClassRows : chainTypeRows)
|
|
11816
11951
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
11817
|
-
for (const subj of
|
|
11952
|
+
for (const subj of chaseSubjects) {
|
|
11818
11953
|
const chain = findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassSucc, { maxHops: 2 });
|
|
11819
11954
|
if (!chain) continue;
|
|
11820
11955
|
const chainRefusal = disjointRefusalFor(subj);
|
|
@@ -11835,7 +11970,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
11835
11970
|
// someValuesFrom chases keep their original, narrower discipline.
|
|
11836
11971
|
const mixedFactForStep = (step) => (step.predicate === SC_PREDICATE ? mixedSubClassRows : mixedTypeRows)
|
|
11837
11972
|
.find((f) => f.subject === step.subject && f.object === step.object);
|
|
11838
|
-
for (const subj of
|
|
11973
|
+
for (const subj of chaseSubjects) {
|
|
11839
11974
|
const chain = findIsaChain(subj, objVariants, mixedTypeEdges, mixedSubClassSucc, { maxHops: 2 });
|
|
11840
11975
|
if (!chain) continue;
|
|
11841
11976
|
const chainRefusal = disjointRefusalFor(subj);
|
|
@@ -12015,7 +12150,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
12015
12150
|
}
|
|
12016
12151
|
: undefined;
|
|
12017
12152
|
};
|
|
12018
|
-
for (const subj of
|
|
12153
|
+
for (const subj of chaseSubjects) {
|
|
12019
12154
|
const chain = findIsaChain(subj, objVariants, chainTypeEdges, enlargedSubClassSucc, { maxHops: 3 });
|
|
12020
12155
|
if (!chain) continue;
|
|
12021
12156
|
const premises = chain.map(factForStepOrSvf);
|
|
@@ -12056,8 +12191,11 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
12056
12191
|
// exists, and telling someone to teach a fact that already follows from
|
|
12057
12192
|
// what they taught is the mirror lie. The probe reads the SAME taught
|
|
12058
12193
|
// edge lists the chases use, and /syllogise closes over a superset of
|
|
12059
|
-
// them, so a chain found here is one it can really materialize.
|
|
12060
|
-
|
|
12194
|
+
// them, so a chain found here is one it can really materialize. The
|
|
12195
|
+
// screened subject list is what the probe walks for that reason: a chain
|
|
12196
|
+
// across a sense join is one /syllogise's own gate would refuse, so
|
|
12197
|
+
// offering it here would send the reader after a fact that never lands.
|
|
12198
|
+
const deeperChainExists = chaseSubjects.some(
|
|
12061
12199
|
(subj) => findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassSucc, { maxHops: DEEP_CHAIN_PROBE_HOPS }),
|
|
12062
12200
|
);
|
|
12063
12201
|
// TRACK B1 — the automatic /prove fallback: every live chase above has
|
|
@@ -17174,7 +17312,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
17174
17312
|
lines.push('taught actions: none yet — teach one ("you can move a disk onto a peg.") and /plan can use it.');
|
|
17175
17313
|
} else {
|
|
17176
17314
|
lines.push("taught actions (planned over, never dispatched):");
|
|
17177
|
-
for (const [familyName, family] of [...families.entries()].sort(([a], [b]) => a
|
|
17315
|
+
for (const [familyName, family] of [...families.entries()].sort(([a], [b]) => byCodepoint(a, b))) {
|
|
17178
17316
|
const cap = capabilityFromActionRules(familyName, family);
|
|
17179
17317
|
const sig = cap.parameters
|
|
17180
17318
|
.map((p) => `${p.name}: ${p.classes.filter(Boolean).join("|") || "?"}`)
|
|
@@ -19129,7 +19267,9 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
|
|
|
19129
19267
|
const separated = separateChoiceTie(grounded, constraintTerms, rowsAfterPull);
|
|
19130
19268
|
if (separated) {
|
|
19131
19269
|
const { winner, runnerUp, missedByRunnerUp } = separated;
|
|
19132
|
-
const groundingFact = winner.facts.length > 0
|
|
19270
|
+
const groundingFact = winner.facts.length > 0
|
|
19271
|
+
? winner.facts.slice().sort((a, b) => (b.trust - a.trust) || compareFactsByContent(a, b))[0]
|
|
19272
|
+
: null;
|
|
19133
19273
|
const groundingClause = groundingFact
|
|
19134
19274
|
? `${factPhrase(groundingFact)} (source: ${citationProvenance(groundingFact.provenance)})`
|
|
19135
19275
|
: renderIsaChain(winner.chain);
|
|
@@ -19170,7 +19310,7 @@ async function dispatchTurn(input, { config, source = defaultSource, graph = nul
|
|
|
19170
19310
|
// edge exists for the winning option — probeChoiceOptions' own guard.
|
|
19171
19311
|
let text; let matches; let hop; let traversalNote; let matchedBy;
|
|
19172
19312
|
if (winner.facts.length > 0) {
|
|
19173
|
-
const fact = winner.facts.slice().sort((a, b) => b.trust - a.trust)[0];
|
|
19313
|
+
const fact = winner.facts.slice().sort((a, b) => (b.trust - a.trust) || compareFactsByContent(a, b))[0];
|
|
19174
19314
|
text = `${winner.text} — ${factPhrase(fact)} (source: ${citationProvenance(fact.provenance)}).`;
|
|
19175
19315
|
matches = winner.facts;
|
|
19176
19316
|
hop = 1;
|