@polycode-projects/the-mechanical-code-talker 1.5.5 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -14
- package/ROADMAP.md +233 -1392
- package/bin/tmct.mjs +479 -98
- package/corpus/README.md +3 -0
- package/corpus/generated/README.md +43 -0
- package/corpus/generated/ace-surface-variants.jsonl +17 -0
- package/corpus/generated/manifest.json +9 -0
- package/corpus/tier2/generate.mjs +14668 -0
- package/corpus/tier2/human-examples-large.jsonl +1928 -0
- package/corpus/tier2/human-examples-medium.jsonl +356 -0
- package/corpus/tier2/human-examples.jsonl +120 -0
- package/corpus/tier2/human-large.jsonl +12001 -0
- package/corpus/tier2/human-medium.jsonl +944 -0
- package/corpus/tier2/human.jsonl +664 -0
- package/corpus/tier2/manifest.json +42 -0
- package/package.json +14 -8
- package/src/answer-variants.json +47 -0
- package/src/answer-variants.mjs +67 -0
- package/src/ask-browser-entry.mjs +34 -0
- package/src/ask-browser.bundle.js +5095 -0
- package/src/ask-vocab.mjs +93 -8
- package/src/ask.mjs +451 -49
- package/src/chat.mjs +1273 -137
- package/src/cli-args.mjs +164 -0
- package/src/codegraph.mjs +170 -32
- package/src/extensions.mjs +100 -19
- package/src/grammar/ace.mjs +85 -3
- package/src/grammar/lexicon-core.json +9531 -63
- package/src/grammar/lexicon.mjs +58 -8
- package/src/graph-merge.mjs +114 -0
- package/src/index.mjs +14 -0
- package/src/init.mjs +40 -14
- package/src/interpret/normalize.mjs +75 -1
- package/src/interpret/strategies/grammar.mjs +10 -0
- package/src/interpret/strategies/keywords.mjs +20 -0
- package/src/interpret/strategies/noise-strip.mjs +73 -4
- package/src/memory/core.mjs +466 -8
- package/src/router/goal-reasoner.mjs +41 -7
- package/src/router/guardrail.mjs +37 -7
- package/src/router/resolver.mjs +50 -4
- package/src/sessions.mjs +5 -1
- package/src/source.mjs +54 -1
- package/src/syllogise.mjs +398 -27
- package/src/toml-config.mjs +13 -4
- package/src/viz.mjs +541 -0
package/src/syllogise.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// INF-A2) without that being "the batch pass on the hot path" — nothing is
|
|
12
12
|
// written to memory unless `syllogise()` itself is called.
|
|
13
13
|
//
|
|
14
|
-
// Deliberately narrow (the plan's kill-criterion discipline):
|
|
14
|
+
// Deliberately narrow (the plan's kill-criterion discipline): FIVE rules —
|
|
15
15
|
// - scm-sco: rdfs:subClassOf transitivity — (a ⊑ b), (b ⊑ c) ⊨ (a ⊑ c).
|
|
16
16
|
// The simplest OWL 2 RL rule, exactly what the ACE grammar's pattern 1
|
|
17
17
|
// emits (`deriveSubClassClosure`).
|
|
@@ -39,8 +39,20 @@
|
|
|
39
39
|
// then cax-sco across `⊑ N3`, three more rules deep. That composition is
|
|
40
40
|
// a documented follow-up (PLAN_INFERENCE_TESTING.md §4 stage 4's
|
|
41
41
|
// remaining scm-svf row), not attempted here (`deriveSomeValuesFromApplication`).
|
|
42
|
+
// - scm-svf1: someValuesFrom restriction SUBSUMPTION — two INDEPENDENTLY
|
|
43
|
+
// declared restrictions over the SAME property, whose filler classes are
|
|
44
|
+
// ⊑-related, entail the restriction NODES are themselves ⊑-related (W3C
|
|
45
|
+
// OWL 2 RL Table 9's scm-svf1 — confirmed, against the real downloaded
|
|
46
|
+
// spec, a DISTINCT rule from scm-svf2, which needs `rdfs:subPropertyOf`
|
|
47
|
+
// instead; tmct's ACE grammar has no way to teach property subsumption at
|
|
48
|
+
// all, so scm-svf2 is out of scope for a real reason, not laziness). Joined
|
|
49
|
+
// the batch pass in a follow-up build (this session): originally deferred
|
|
50
|
+
// (INFBENCH's two drive points never touched the batch pass, so nothing
|
|
51
|
+
// was lost by deferring), closed once a positive INFBENCH fixture case
|
|
52
|
+
// (`c1ScmSvfApply`) existed to measure it against
|
|
53
|
+
// (`deriveSomeValuesFromSubsumption`).
|
|
42
54
|
//
|
|
43
|
-
// The whole safety story is four guards, all mechanical, shared by all
|
|
55
|
+
// The whole safety story is four guards, all mechanical, shared by all five rules:
|
|
44
56
|
// - BUDGET — at most `budget` NEW derivations per pass (default 50), and at
|
|
45
57
|
// most `depth` fixpoint rounds (scm-sco only — cax-sco/cax-dw need no
|
|
46
58
|
// fixpoint, see their own doc comments). The pass stops at whichever bites
|
|
@@ -54,17 +66,40 @@
|
|
|
54
66
|
// content key — the same novelty test appendFact's content-hash id enforces).
|
|
55
67
|
//
|
|
56
68
|
// Every derived fact is written via appendFacts with `entailed:subClassOf`
|
|
57
|
-
// (scm-sco), `entailed:type` (cax-sco), `entailed:disjointWith` (cax-dw),
|
|
58
|
-
// `entailed:someValuesFrom` (cls-svf1)
|
|
59
|
-
// Source, trust prior 0.3 in
|
|
60
|
-
// outranks a stated fact, and is
|
|
61
|
-
// source graph moves. cax-dw's
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
69
|
+
// (scm-sco), `entailed:type` (cax-sco), `entailed:disjointWith` (cax-dw),
|
|
70
|
+
// `entailed:someValuesFrom` (cls-svf1), or `entailed:someValuesFromSubsumption`
|
|
71
|
+
// (scm-svf1) provenance (a first-class entailed Source, trust prior 0.3 in
|
|
72
|
+
// memory/trust.mjs) so it is LOW trust, NEVER outranks a stated fact, and is
|
|
73
|
+
// fully RETRACTABLE by provenance when the source graph moves. cax-dw's,
|
|
74
|
+
// cls-svf1's, and scm-svf1's conclusions additionally ride trust.mjs's
|
|
75
|
+
// entailed hook when their OWN premises are resolvable in the pre-pass
|
|
76
|
+
// snapshot — premise-derived (`min(premiseTrusts) × ruleConfidence`), still
|
|
77
|
+
// always strictly below its weakest premise (see syllogise()'s own doc
|
|
65
78
|
// comment) — rather than the bare entailed floor; scm-sco/cax-sco do not
|
|
66
79
|
// (yet) engage that hook — see syllogise()'s doc comment for why that
|
|
67
80
|
// specific extension is non-trivial.
|
|
81
|
+
//
|
|
82
|
+
// TWO MORE capabilities live below (PLAN_INFERENCE_TESTING.md §4 stage 4's
|
|
83
|
+
// remainder, INF-C1), both LIVE-CHASE ONLY — never added to `syllogise()`'s own
|
|
84
|
+
// materializing batch pass (documented as a deliberate design choice at each
|
|
85
|
+
// export's own doc comment, not an oversight — see each one for the specific
|
|
86
|
+
// reason: neither is an enumerable "derive every new fact of this shape"
|
|
87
|
+
// closure the way the five rules above are):
|
|
88
|
+
// - cardinality monotonicity: a class's OWN declared exactly/min cardinality
|
|
89
|
+
// restriction (n) proves "at least m" for any QUERIED m ≤ n. Confirmed
|
|
90
|
+
// OUTSIDE OWL 2 RL's own decidable profile (the spec's own `cls-*` rule
|
|
91
|
+
// table has no rule comparing exactly/min/max cardinalities to each
|
|
92
|
+
// other, and the profile's syntactic restriction limits cardinality
|
|
93
|
+
// expressions to 0 or 1 only) — genuinely LIVE-CHASE ONLY, never a
|
|
94
|
+
// candidate for the batch pass at all, not merely deferred
|
|
95
|
+
// (`proveCardinalityAtLeast`).
|
|
96
|
+
// - cax-maxc0: max-cardinality-0 as encoded negation. Grounded in W3C OWL 2
|
|
97
|
+
// RL's real `cls-maxc1` rule (an ABox contradiction: asserting a specific
|
|
98
|
+
// individual has a value for a max-0-restricted property is
|
|
99
|
+
// inconsistent) via a one-step universal generalization — since ANY
|
|
100
|
+
// witnessed individual would be a contradiction, no true witness can
|
|
101
|
+
// exist, so a general "no" is provable without needing one
|
|
102
|
+
// (`proveMaxCardinalityZeroDenial`).
|
|
68
103
|
|
|
69
104
|
import { loadMemory, appendFacts, readFactRows, normFactTerm } from "./memory/core.mjs";
|
|
70
105
|
|
|
@@ -129,6 +164,7 @@ const isType = (p) => String(p || "").trim().toLowerCase() === "rdf:type";
|
|
|
129
164
|
const isDisjoint = (p) => String(p || "").trim().toLowerCase() === "owl:disjointwith";
|
|
130
165
|
const isOnProperty = (p) => String(p || "").trim().toLowerCase() === "owl:onproperty";
|
|
131
166
|
const isSomeValuesFrom = (p) => String(p || "").trim().toLowerCase() === "owl:somevaluesfrom";
|
|
167
|
+
const isOnClass = (p) => String(p || "").trim().toLowerCase() === "owl:onclass";
|
|
132
168
|
/** The four structural OWL predicates cls-svf1 itself consumes/emits, plus
|
|
133
169
|
* the two subClassOf/type/disjointWith predicates the other three rules
|
|
134
170
|
* own — excluded from `syllogise()`'s generic "property edge" scan so a
|
|
@@ -138,6 +174,30 @@ const RESERVED_PREDICATES = new Set([
|
|
|
138
174
|
"rdfs:subclassof", "rdf:type", "owl:disjointwith", "owl:onproperty", "owl:somevaluesfrom", "owl:intersectionof",
|
|
139
175
|
]);
|
|
140
176
|
|
|
177
|
+
/**
|
|
178
|
+
* PURE `min(premiseTrusts) × ruleConfidence` — the entailed hook's own core
|
|
179
|
+
* arithmetic (memory/trust.mjs `computeTrust`), exposed here so a caller that
|
|
180
|
+
* has NO Fact/appendFacts call to delegate to (a LIVE, read-only chat proof
|
|
181
|
+
* chase — chat.mjs's scm-svf1/cardinality-monotonicity/cax-maxc0 call sites)
|
|
182
|
+
* can still compute the SAME premise-derived figure `computeTrust` would land
|
|
183
|
+
* on for a materialized fact (recency sits at ~1.0 for a freshly-derived
|
|
184
|
+
* conclusion, so the two agree). `syllogise()`'s own `appendFacts` mapping
|
|
185
|
+
* does NOT call this — it hands `premiseTrusts`/`ruleConfidence` straight to
|
|
186
|
+
* `appendFacts` → `computeTrust`, which ALSO applies the recency nudge; this
|
|
187
|
+
* helper is for the read-only case that has no Fact to nudge. Returns `null`
|
|
188
|
+
* when no numeric premise trust was supplied (nothing to compute from — never
|
|
189
|
+
* a magic default), else clamped to [0,1]. Pure, no I/O.
|
|
190
|
+
*/
|
|
191
|
+
export function entailedTrustFrom(premiseTrusts, ruleConfidence = 1) {
|
|
192
|
+
const nums = (Array.isArray(premiseTrusts) ? premiseTrusts : []).filter((t) => typeof t === "number");
|
|
193
|
+
if (!nums.length) return null;
|
|
194
|
+
const clamped = Math.max(0, Math.min(1, Math.min(...nums) * ruleConfidence));
|
|
195
|
+
// rounded to 6dp — mirrors memory/trust.mjs's own `round(n, 6)` convention,
|
|
196
|
+
// so a live-chase figure reads identically to what computeTrust would have
|
|
197
|
+
// stored had this conclusion been persisted.
|
|
198
|
+
return Number(clamped.toFixed(6));
|
|
199
|
+
}
|
|
200
|
+
|
|
141
201
|
/** Normalize a focus hint (Set|array of terms) into the same normalized-term
|
|
142
202
|
* space stored facts live in, or null for "no focus → whole graph". */
|
|
143
203
|
function normalizeFocus(focus) {
|
|
@@ -482,6 +542,272 @@ export function deriveSomeValuesFromApplication(propertyEdges, typeEdges, subCla
|
|
|
482
542
|
return derived;
|
|
483
543
|
}
|
|
484
544
|
|
|
545
|
+
// ---- shared cardinality-restriction reconstruction (pattern-5, parseCardinality) ----
|
|
546
|
+
// "every N1 has exactly n N2s" already stores `{N1, rdfs:subClassOf, r}` as a
|
|
547
|
+
// plain row (`r = tmct:exactly-N-n2`) plus the restriction node r's own
|
|
548
|
+
// scaffolding: `r owl:onProperty tmct:has`, `r <kind> "n"` (kind one of
|
|
549
|
+
// owl:cardinality/owl:minCardinality/owl:maxCardinality), `r owl:onClass N2`
|
|
550
|
+
// (`src/grammar/ace.mjs`'s `parseCardinality`, ~lines 234-257) — the SAME
|
|
551
|
+
// per-fact storage discipline `deriveSomeValuesFromApplication`'s
|
|
552
|
+
// `restrictionEdges` already reconstructs for someValuesFrom restrictions,
|
|
553
|
+
// applied to pattern 5's shape instead.
|
|
554
|
+
const HAS_PROPERTY_KEY = "has"; // the fixed synthetic marker property parseCardinality always mints (ace.mjs ~line 252) — never a real taught verb, so it doubles as this reconstruction's own defensive filter (see buildCardinalityRestrictions below): a someValuesFrom restriction's onProperty is always a REAL taught verb, never this literal marker.
|
|
555
|
+
const CARDINALITY_KIND_OF = { "owl:cardinality": "exactly", "owl:mincardinality": "min", "owl:maxcardinality": "max" };
|
|
556
|
+
export const ON_CLASS_PREDICATE = "owl:onClass";
|
|
557
|
+
|
|
558
|
+
/** Reconstructs pattern-5 cardinality restriction records from raw stored
|
|
559
|
+
* rows touching a restriction node — same reconstruction discipline
|
|
560
|
+
* `deriveSomeValuesFromApplication` already uses for someValuesFrom
|
|
561
|
+
* restrictions, applied to pattern 5's shape instead. `rows` is the same
|
|
562
|
+
* `[{subject,predicate,object}, …]` shape `readFactRows`/`syllogise()`'s own
|
|
563
|
+
* `rows` param takes (raw predicate casing, already-normalized subject/
|
|
564
|
+
* object) — a caller may hand it EVERY stored row (this function ignores
|
|
565
|
+
* anything that isn't one of the four predicates it cares about) or a
|
|
566
|
+
* pre-filtered subset. A restriction is only admitted when its OWN
|
|
567
|
+
* `owl:onProperty` row resolves to `HAS_PROPERTY_KEY` — the defensive belt
|
|
568
|
+
* that keeps a someValuesFrom restriction's scaffolding (which ALSO uses
|
|
569
|
+
* `owl:onProperty`, just with a real verb) from ever being mistaken for a
|
|
570
|
+
* cardinality restriction when both kinds' rows are scanned together (e.g.
|
|
571
|
+
* chat.mjs's live wiring, which reads the whole taught-fact set at once).
|
|
572
|
+
* Returns `[{ restriction, kind, n, onClass }, …]`, deterministic order
|
|
573
|
+
* (sorted by restriction id). Pure, no I/O. */
|
|
574
|
+
export function buildCardinalityRestrictions(rows) {
|
|
575
|
+
const onPropertyOf = new Map(); // restriction -> owl:onProperty's object
|
|
576
|
+
const kindOf = new Map(); // restriction -> { kind, n }
|
|
577
|
+
const onClassOf = new Map(); // restriction -> owl:onClass's object
|
|
578
|
+
for (const r of rows || []) {
|
|
579
|
+
if (!r || !r.subject || !r.predicate) continue;
|
|
580
|
+
if (isOnProperty(r.predicate)) onPropertyOf.set(r.subject, r.object);
|
|
581
|
+
else if (isOnClass(r.predicate)) onClassOf.set(r.subject, r.object);
|
|
582
|
+
else {
|
|
583
|
+
const kind = CARDINALITY_KIND_OF[String(r.predicate).trim().toLowerCase()];
|
|
584
|
+
if (!kind) continue;
|
|
585
|
+
const n = Number(r.object);
|
|
586
|
+
if (Number.isFinite(n)) kindOf.set(r.subject, { kind, n });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const restrictions = [];
|
|
590
|
+
for (const [restriction, { kind, n }] of kindOf) {
|
|
591
|
+
if (onPropertyOf.get(restriction) !== HAS_PROPERTY_KEY) continue; // not a cardinality restriction's own onProperty row — skip (the defensive belt, see doc comment)
|
|
592
|
+
const onClass = onClassOf.get(restriction);
|
|
593
|
+
if (!onClass) continue;
|
|
594
|
+
restrictions.push({ restriction, kind, n, onClass });
|
|
595
|
+
}
|
|
596
|
+
restrictions.sort((a, b) => a.restriction.localeCompare(b.restriction));
|
|
597
|
+
return restrictions;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---- scm-svf1: someValuesFrom restriction subsumption (W3C OWL 2 RL Table 9,
|
|
601
|
+
// scm-svf1 — confirmed distinct from scm-svf2, which needs property
|
|
602
|
+
// subsumption tmct can't teach yet, see this file's header comment) ----
|
|
603
|
+
export const SCM_SVF_RULE = "someValuesFromSubsumption";
|
|
604
|
+
export const ENTAILED_SCM_SVF_PROVENANCE = `entailed:${SCM_SVF_RULE}`;
|
|
605
|
+
/** trust.mjs's entailed hook's rule-confidence for scm-svf1 — the same sub-1
|
|
606
|
+
* discount cax-dw/cls-svf1 use and for the identical reason (see
|
|
607
|
+
* CAX_DW_RULE_CONFIDENCE's own comment): keeps a premise-derived conclusion
|
|
608
|
+
* STRICTLY below its weakest premise's trust, every time. Wired into
|
|
609
|
+
* `syllogise()`'s own materializing pass (below) — scm-svf1 joined the batch
|
|
610
|
+
* pass in a follow-up build, see this file's header comment. */
|
|
611
|
+
export const SCM_SVF_RULE_CONFIDENCE = 0.95;
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* PURE scm-svf1: c1 someValuesFrom y1, c1 onProperty p, c2 someValuesFrom y2,
|
|
615
|
+
* c2 onProperty p, y1 ⊑ y2 (lifted through y1's FULL ⊑-ancestor closure, same
|
|
616
|
+
* lift discipline as cax-dw/cls-svf1) |= c1 ⊑ c2 — a schema-level fact about
|
|
617
|
+
* the restriction NODES themselves, TWO independently-declared restrictions
|
|
618
|
+
* being required to compare (unlike cardinality monotonicity/cax-maxc0 below,
|
|
619
|
+
* each sufficient from a SINGLE declared restriction). `restrictionEdges` is
|
|
620
|
+
* the SAME `[{ restriction, property, target }, …]` shape
|
|
621
|
+
* `deriveSomeValuesFromApplication` already takes (`property`/`target`
|
|
622
|
+
* already normFactTerm-normalized); `subClassEdges` is the ordinary
|
|
623
|
+
* `[[a,b], …]` shape every other rule in this file takes, a FIXED input never
|
|
624
|
+
* mutated by this function (so no fixpoint rounds are needed, same reasoning
|
|
625
|
+
* as `deriveTypePropagation`'s own doc comment). Restrictions are grouped by
|
|
626
|
+
* their (normalized) property — only restrictions sharing the SAME property
|
|
627
|
+
* are ever compared, matching the rule's own premise shape (`c1 onProperty p`,
|
|
628
|
+
* `c2 onProperty p`, the SAME p).
|
|
629
|
+
*
|
|
630
|
+
* Deliberately LIVE-CHASE ONLY for this build (see this file's header
|
|
631
|
+
* comment): never added to `syllogise()`'s materializing batch pass.
|
|
632
|
+
*
|
|
633
|
+
* Returns ONLY new `{ subject, object, viaY1, viaY2 }` conclusions (`subject`/
|
|
634
|
+
* `object` are the two restriction node ids, `viaY1`/`viaY2` the specific
|
|
635
|
+
* filler classes whose ⊑-relation licensed it), bounded by `budget`,
|
|
636
|
+
* focus-filtered, tautology- and dedup-screened, deterministic order. No I/O.
|
|
637
|
+
*/
|
|
638
|
+
export function deriveSomeValuesFromSubsumption(restrictionEdges, subClassEdges, { budget = 50, focus = null } = {}) {
|
|
639
|
+
const ancestorsOf = buildAncestorCloser(subClassEdges);
|
|
640
|
+
|
|
641
|
+
const byProperty = new Map(); // normalized property -> [{ restriction, target }]
|
|
642
|
+
for (const r of restrictionEdges || []) {
|
|
643
|
+
if (!r || !r.restriction || !r.property || !r.target) continue;
|
|
644
|
+
const pKey = normFactTerm(r.property);
|
|
645
|
+
if (!byProperty.has(pKey)) byProperty.set(pKey, []);
|
|
646
|
+
byProperty.get(pKey).push({ restriction: r.restriction, target: r.target });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const present = new Set(); // "c1\0c2" for a subClassOf edge already known between two restriction nodes (dedup/novelty screen, same discipline as every other rule here)
|
|
650
|
+
for (const [a, b] of subClassEdges || []) if (a && b) present.add(`${a}${SEP}${b}`);
|
|
651
|
+
|
|
652
|
+
const focusSet = focus instanceof Set ? (focus.size ? focus : null) : normalizeFocus(focus);
|
|
653
|
+
const inFocus = (c1, c2) => !focusSet || focusSet.has(c1) || focusSet.has(c2);
|
|
654
|
+
|
|
655
|
+
const candidates = [];
|
|
656
|
+
for (const [, group] of byProperty) {
|
|
657
|
+
if (group.length < 2) continue; // need TWO independently-declared restrictions to compare
|
|
658
|
+
for (const r1 of group) {
|
|
659
|
+
for (const r2 of group) {
|
|
660
|
+
if (r1.restriction === r2.restriction) continue; // tautology screen
|
|
661
|
+
if (r1.target === r2.target) continue; // same filler, same node by construction — no new fact
|
|
662
|
+
if (!ancestorsOf(r1.target).has(r2.target)) continue; // y1 must be ⊑ y2 (the FULL ⊑-lift)
|
|
663
|
+
const key = `${r1.restriction}${SEP}${r2.restriction}`;
|
|
664
|
+
if (present.has(key)) continue; // dedup / novelty screen
|
|
665
|
+
if (!inFocus(r1.restriction, r2.restriction)) continue; // focus-connection screen
|
|
666
|
+
candidates.push([r1.restriction, r2.restriction, r1.target, r2.target, key]);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
candidates.sort((a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]));
|
|
671
|
+
const derived = [];
|
|
672
|
+
const derivedKeys = new Set();
|
|
673
|
+
for (const [c1, c2, y1, y2, key] of candidates) {
|
|
674
|
+
if (derivedKeys.has(key)) continue;
|
|
675
|
+
if (derived.length >= budget) break;
|
|
676
|
+
derivedKeys.add(key);
|
|
677
|
+
derived.push({ subject: c1, object: c2, viaY1: y1, viaY2: y2 });
|
|
678
|
+
}
|
|
679
|
+
return derived;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ---- shared machinery for cardinality monotonicity / cax-maxc0: both are
|
|
683
|
+
// single-premise-sufficient (no SECOND independently-taught restriction
|
|
684
|
+
// needed, unlike scm-svf1 above) — a class's OWN declared cardinality
|
|
685
|
+
// restriction, walked through its FULL ⊑-ancestor closure (same lift
|
|
686
|
+
// discipline as every other rule in this file). ----
|
|
687
|
+
|
|
688
|
+
/** Shared bounded proof search for `proveCardinalityAtLeast`/
|
|
689
|
+
* `proveMaxCardinalityZeroDenial`: walks `subject`'s OWN ⊑-ancestor closure
|
|
690
|
+
* (itself first, then ancestors — the FULL lift, same discipline as cax-dw/
|
|
691
|
+
* cls-svf1) looking for a class with a DIRECTLY declared cardinality
|
|
692
|
+
* restriction satisfying `matches(record)`. `cardinalityRestrictionEdges` is
|
|
693
|
+
* `buildCardinalityRestrictions`'s own output shape. Returns the first
|
|
694
|
+
* `{ viaClass, viaRestriction, record }` found (deterministic — the ancestor
|
|
695
|
+
* walk's order is fixed for a given edge set) or null. `budget` bounds how
|
|
696
|
+
* many candidate classes (subject + ancestors) are examined, a QUERY-rooted
|
|
697
|
+
* proof search in the same spirit as `findIsaChain`'s `maxHops`, not a
|
|
698
|
+
* batch-derivation cap. Pure, no I/O. */
|
|
699
|
+
function findOwnCardinalityRestriction(subClassEdges, cardinalityRestrictionEdges, subject, matches, { budget = 20, focus = null } = {}) {
|
|
700
|
+
if (!subject) return null;
|
|
701
|
+
const ancestorsOf = buildAncestorCloser(subClassEdges);
|
|
702
|
+
const focusSet = focus instanceof Set ? (focus.size ? focus : null) : normalizeFocus(focus);
|
|
703
|
+
const inFocus = (c) => !focusSet || focusSet.has(subject) || focusSet.has(c);
|
|
704
|
+
const classToRestrictions = new Map(); // class -> Set(restriction ids DIRECTLY declared class ⊑ restriction)
|
|
705
|
+
for (const [a, b] of subClassEdges || []) {
|
|
706
|
+
if (!a || !b) continue;
|
|
707
|
+
if (!classToRestrictions.has(a)) classToRestrictions.set(a, new Set());
|
|
708
|
+
classToRestrictions.get(a).add(b);
|
|
709
|
+
}
|
|
710
|
+
const restrictionsByRid = new Map((cardinalityRestrictionEdges || []).map((r) => [r.restriction, r]));
|
|
711
|
+
let checked = 0;
|
|
712
|
+
for (const c of [subject, ...ancestorsOf(subject)]) {
|
|
713
|
+
if (checked >= budget) break;
|
|
714
|
+
checked += 1;
|
|
715
|
+
if (!inFocus(c)) continue;
|
|
716
|
+
for (const rid of classToRestrictions.get(c) || []) {
|
|
717
|
+
const rec = restrictionsByRid.get(rid);
|
|
718
|
+
if (rec && matches(rec)) return { viaClass: c, viaRestriction: rid, record: rec };
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ---- cardinality monotonicity (confirmed OUTSIDE OWL 2 RL's own decidable
|
|
725
|
+
// profile, see this file's header comment) ----
|
|
726
|
+
export const SCM_CARD_RULE = "cardinalityMonotonicity";
|
|
727
|
+
/** trust.mjs's entailed hook's rule-confidence for cardinality monotonicity —
|
|
728
|
+
* the same sub-1 discount every other rule on this ladder uses (see
|
|
729
|
+
* CAX_DW_RULE_CONFIDENCE's own comment). Unlike CAX_DW_RULE_CONFIDENCE/
|
|
730
|
+
* CLS_SVF1_RULE_CONFIDENCE/SCM_SVF_RULE_CONFIDENCE, this constant has no
|
|
731
|
+
* `syllogise()`/`appendFacts` call site to feed: `proveCardinalityAtLeast` is
|
|
732
|
+
* QUERY-rooted (see its own doc comment) and never produces an enumerable
|
|
733
|
+
* Fact for the entailed hook to score — there is no `mgx:trustScore` for this
|
|
734
|
+
* rule's answer to carry. Defined here anyway, for the same reason every
|
|
735
|
+
* other rule-confidence constant is a named export rather than an inline
|
|
736
|
+
* literal: chat.mjs's LIVE proof chase (the only caller) computes and
|
|
737
|
+
* attaches a `min(premiseTrusts) × ruleConfidence` figure to its OWN answer
|
|
738
|
+
* for auditability (`entailedTrustFrom`, below), even though today's answer
|
|
739
|
+
* plumbing does not yet surface it past that one function's return value. */
|
|
740
|
+
export const CARDINALITY_RULE_CONFIDENCE = 0.95;
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* PURE: given one class `subject`'s OWN declared cardinality restriction
|
|
744
|
+
* (kind ∈ {exactly,min}, n, onClass — lifted through `subject`'s FULL
|
|
745
|
+
* ⊑-ancestor closure, so an inherited restriction counts too) and a QUERIED
|
|
746
|
+
* (`onClass`, `m`), proves "`subject` has at least `m` `onClass`" whenever
|
|
747
|
+
* `onClass` matches and `n ≥ m`. A bounded, QUERY-rooted proof (there is no
|
|
748
|
+
* fixed enumerable "new fact" to write — `m` is query-specific, a different
|
|
749
|
+
* shape than every derivation-producing rule above) — genuinely LIVE-CHASE
|
|
750
|
+
* ONLY (see this file's header comment: this is outside OWL 2 RL's own
|
|
751
|
+
* profile, and unlike scm-svf1, this one has no enumerable fact shape to ever
|
|
752
|
+
* join the batch pass — not merely deferred).
|
|
753
|
+
*
|
|
754
|
+
* Returns the witnessing `{ subject, object: onClass, m, n, kind, viaClass,
|
|
755
|
+
* viaRestriction }` or null (`viaClass` is the specific class in `subject`'s
|
|
756
|
+
* ⊑-closure the restriction was actually declared against — itself, for a
|
|
757
|
+
* direct hit, or an ancestor, for the lift). No I/O.
|
|
758
|
+
*/
|
|
759
|
+
export function proveCardinalityAtLeast(subClassEdges, cardinalityRestrictionEdges, subject, onClass, m, opts = {}) {
|
|
760
|
+
if (!onClass || !Number.isFinite(m)) return null;
|
|
761
|
+
const found = findOwnCardinalityRestriction(
|
|
762
|
+
subClassEdges, cardinalityRestrictionEdges, subject,
|
|
763
|
+
(rec) => rec.onClass === onClass && (rec.kind === "exactly" || rec.kind === "min") && rec.n >= m,
|
|
764
|
+
opts,
|
|
765
|
+
);
|
|
766
|
+
return found ? { subject, object: onClass, m, n: found.record.n, kind: found.record.kind, viaClass: found.viaClass, viaRestriction: found.viaRestriction } : null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ---- cax-maxc0: max-cardinality-0 as encoded negation (grounded in the real
|
|
770
|
+
// W3C OWL 2 RL `cls-maxc1` ABox contradiction rule via a one-step universal
|
|
771
|
+
// generalization — see this file's header comment; `cax-` prefix per this
|
|
772
|
+
// ladder's "produces a provable no" naming convention, same epistemic status
|
|
773
|
+
// as cax-dw) ----
|
|
774
|
+
export const CAX_MAXC0_RULE = "maxCardinalityZero";
|
|
775
|
+
/** trust.mjs's entailed hook's rule-confidence for cax-maxc0 — same sub-1
|
|
776
|
+
* discount, same reason (CAX_DW_RULE_CONFIDENCE's own comment). Same caveat
|
|
777
|
+
* as CARDINALITY_RULE_CONFIDENCE just above: `proveMaxCardinalityZeroDenial`
|
|
778
|
+
* is QUERY-rooted and never produces an enumerable Fact either, so there is
|
|
779
|
+
* no `mgx:trustScore` for this rule to carry — chat.mjs's LIVE proof chase
|
|
780
|
+
* computes and attaches the `min(premiseTrusts) × ruleConfidence` figure to
|
|
781
|
+
* its own answer for auditability (`entailedTrustFrom`, below). */
|
|
782
|
+
export const CAX_MAXC0_RULE_CONFIDENCE = 0.95;
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* PURE: `subject` ⊑ r (lifted through `subject`'s FULL ⊑-ancestor closure), r
|
|
786
|
+
* a maxCardinality-0 restriction (property `has`, onClass `onClass`) |= "no
|
|
787
|
+
* `subject` has a `onClass`" — the universal-generalization bridge from
|
|
788
|
+
* `cls-maxc1`'s per-individual ABox contradiction (asserting a witnessed
|
|
789
|
+
* individual would be inconsistent) to a class-level provable negative: since
|
|
790
|
+
* NO witness can exist without contradiction, the general "no" is sound.
|
|
791
|
+
* Same query-rooted, LIVE-CHASE-ONLY scope as `proveCardinalityAtLeast`
|
|
792
|
+
* (never `syllogise()`'s batch pass). NEVER infers "no" from absence — a
|
|
793
|
+
* subject with no declared max-0 restriction at all simply returns null
|
|
794
|
+
* (matching cax-dw's own discipline, `deriveDisjointViolations`'s doc
|
|
795
|
+
* comment above).
|
|
796
|
+
*
|
|
797
|
+
* Returns `{ subject, object: onClass, viaClass, viaRestriction }` or null
|
|
798
|
+
* (`viaClass` — itself, for a direct hit, or an ancestor, for the lift). No
|
|
799
|
+
* I/O.
|
|
800
|
+
*/
|
|
801
|
+
export function proveMaxCardinalityZeroDenial(subClassEdges, cardinalityRestrictionEdges, subject, onClass, opts = {}) {
|
|
802
|
+
if (!onClass) return null;
|
|
803
|
+
const found = findOwnCardinalityRestriction(
|
|
804
|
+
subClassEdges, cardinalityRestrictionEdges, subject,
|
|
805
|
+
(rec) => rec.onClass === onClass && rec.kind === "max" && rec.n === 0,
|
|
806
|
+
opts,
|
|
807
|
+
);
|
|
808
|
+
return found ? { subject, object: onClass, viaClass: found.viaClass, viaRestriction: found.viaRestriction } : null;
|
|
809
|
+
}
|
|
810
|
+
|
|
485
811
|
/**
|
|
486
812
|
* PURE consistency checker (PLAN_INFERENCE_TESTING.md S1 INF-C2, S4 stage 5):
|
|
487
813
|
* detects when a SINGLE subject's own already-asserted types contradict each
|
|
@@ -567,28 +893,32 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
|
|
|
567
893
|
/**
|
|
568
894
|
* Run one bounded speculative pass over the memory graph under `repoDir`
|
|
569
895
|
* (the repo dir whose .tmct/memory/graph.json appendFact/loadMemory manage).
|
|
570
|
-
* Reads the stored subClassOf, rdf:type, owl:disjointWith AND (for cls-svf1
|
|
571
|
-
* owl:onProperty/owl:someValuesFrom + every other object-property
|
|
572
|
-
* forward-chains
|
|
573
|
-
* propagation, seeing THIS pass's own scm-sco conclusions too, so a
|
|
574
|
-
* two-hop taught chain and its type propagation both materialize in one
|
|
896
|
+
* Reads the stored subClassOf, rdf:type, owl:disjointWith AND (for cls-svf1/
|
|
897
|
+
* scm-svf1) owl:onProperty/owl:someValuesFrom + every other object-property
|
|
898
|
+
* fact, forward-chains FIVE rules — scm-sco (⊑-transitivity) then cax-sco
|
|
899
|
+
* (type propagation, seeing THIS pass's own scm-sco conclusions too, so a
|
|
900
|
+
* fresh two-hop taught chain and its type propagation both materialize in one
|
|
575
901
|
* call) then cax-dw (disjointness violations, seeing THIS pass's own scm-sco
|
|
576
902
|
* AND cax-sco conclusions too) then cls-svf1 (someValuesFrom restriction
|
|
577
|
-
* membership, also seeing the enlarged subClassOf set for its own ⊑-lift)
|
|
578
|
-
*
|
|
903
|
+
* membership, also seeing the enlarged subClassOf set for its own ⊑-lift)
|
|
904
|
+
* then scm-svf1 (restriction-to-restriction subsumption, seeing the SAME
|
|
905
|
+
* enlarged subClassOf set — its own ⊑-lift over the two restrictions' filler
|
|
906
|
+
* classes) — and materializes each NEW conclusion via `appendFacts` with
|
|
579
907
|
* `entailed:subClassOf`/`entailed:type`/`entailed:disjointWith`/
|
|
580
|
-
* `entailed:someValuesFrom` provenance +
|
|
581
|
-
* stage 2's entailed hook:
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
* when they are not, e.g. a
|
|
585
|
-
*
|
|
586
|
-
*
|
|
908
|
+
* `entailed:someValuesFrom`/`entailed:someValuesFromSubsumption` provenance +
|
|
909
|
+
* trust (PLAN_INFERENCE_TESTING.md S4 stage 2's entailed hook:
|
|
910
|
+
* `min(premiseTrusts) x ruleConfidence` when the conclusion's OWN premises
|
|
911
|
+
* are resolvable in the pre-pass snapshot, falling back to the bare entailed
|
|
912
|
+
* prior — memory/trust.mjs's SOURCE_PRIOR floor — when they are not, e.g. a
|
|
913
|
+
* premise itself only exists because THIS SAME pass just derived it a round
|
|
914
|
+
* earlier; still low, still never outranks a stated fact, just less
|
|
915
|
+
* precisely premise-derived for that one case).
|
|
587
916
|
*
|
|
588
917
|
* opts:
|
|
589
918
|
* - depth max fixpoint rounds (scm-sco chain growth), default 32
|
|
590
|
-
* - budget max NEW derivations written this pass, SHARED across all
|
|
591
|
-
* rules (scm-sco, then cax-sco, then cax-dw, then cls-svf1
|
|
919
|
+
* - budget max NEW derivations written this pass, SHARED across all five
|
|
920
|
+
* rules (scm-sco, then cax-sco, then cax-dw, then cls-svf1, then
|
|
921
|
+
* scm-svf1), default 50
|
|
592
922
|
* - focus Set|array of class terms; when given, only derivations touching
|
|
593
923
|
* focus (subject, pivot, or object ∈ focus) are admitted. Omit for a
|
|
594
924
|
* whole-graph batch pass.
|
|
@@ -675,9 +1005,23 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
675
1005
|
const svf1Derived = remainingBudgetSvf1 > 0 && restrictionEdges.length
|
|
676
1006
|
? deriveSomeValuesFromApplication(propertyEdges, typeEdges, enlargedSubClassEdges, restrictionEdges, { budget: remainingBudgetSvf1, focus: normalizedFocus })
|
|
677
1007
|
: [];
|
|
1008
|
+
// scm-svf1 sees the SAME enlarged subClassOf set (its own ⊑-lift over the
|
|
1009
|
+
// two restrictions' filler classes) and reuses the SAME restrictionEdges
|
|
1010
|
+
// just built for cls-svf1 above — needs at least two independently-declared
|
|
1011
|
+
// restrictions over one property to have anything to compare (the kernel's
|
|
1012
|
+
// own guard, `deriveSomeValuesFromSubsumption`'s doc comment).
|
|
1013
|
+
const remainingBudgetScmSvf = Math.max(0, budget - scmDerived.length - caxDerived.length - dwDerived.length - svf1Derived.length);
|
|
1014
|
+
const scmSvfDerived = remainingBudgetScmSvf > 0 && restrictionEdges.length > 1
|
|
1015
|
+
? deriveSomeValuesFromSubsumption(restrictionEdges, enlargedSubClassEdges, { budget: remainingBudgetScmSvf, focus: normalizedFocus })
|
|
1016
|
+
: [];
|
|
1017
|
+
// scm-svf1's own two structural premises per restriction (owl:onProperty /
|
|
1018
|
+
// owl:someValuesFrom) are looked up by restriction id — restrictionEdges
|
|
1019
|
+
// already carries each restriction's (property, target) pair, keyed the
|
|
1020
|
+
// same way `deriveSomeValuesFromSubsumption`'s own output does.
|
|
1021
|
+
const restrictionByRid = new Map(restrictionEdges.map((r) => [r.restriction, r]));
|
|
678
1022
|
|
|
679
1023
|
// Batched write: ONE mutateMemory pass for the whole pass's conclusions
|
|
680
|
-
// (all
|
|
1024
|
+
// (all five rules), not one appendFact per derived fact — appendFacts (the
|
|
681
1025
|
// appendUtterances-precedent batch path, memory/core.mjs) does the same
|
|
682
1026
|
// normalize+prose-tokenize+upsert work per fact but a SINGLE read-mutate-
|
|
683
1027
|
// write, so a pass with many derivations no longer pays per-fact I/O.
|
|
@@ -743,6 +1087,29 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
743
1087
|
...(premiseTrusts.length ? { premiseTrusts, ruleConfidence: CLS_SVF1_RULE_CONFIDENCE } : {}),
|
|
744
1088
|
};
|
|
745
1089
|
}),
|
|
1090
|
+
...scmSvfDerived.map((d) => {
|
|
1091
|
+
// each restriction's own two structural premises (owl:onProperty /
|
|
1092
|
+
// owl:someValuesFrom rows), for BOTH restrictions being compared, plus
|
|
1093
|
+
// the y1⊑y2 subClassOf premise that licensed the comparison — always
|
|
1094
|
+
// present here (unlike cax-dw/cls-svf1's optional lift premise): the
|
|
1095
|
+
// kernel's own tautology screen guarantees viaY1 !== viaY2 for every
|
|
1096
|
+
// derived scm-svf1 fact (`deriveSomeValuesFromSubsumption`'s doc comment).
|
|
1097
|
+
const r1 = restrictionByRid.get(d.subject);
|
|
1098
|
+
const r2 = restrictionByRid.get(d.object);
|
|
1099
|
+
const premiseTrusts = numericOnly([
|
|
1100
|
+
r1 && premiseTrust(d.subject, ON_PROPERTY_PREDICATE, r1.property),
|
|
1101
|
+
premiseTrust(d.subject, SOME_VALUES_FROM_PREDICATE, d.viaY1),
|
|
1102
|
+
r2 && premiseTrust(d.object, ON_PROPERTY_PREDICATE, r2.property),
|
|
1103
|
+
premiseTrust(d.object, SOME_VALUES_FROM_PREDICATE, d.viaY2),
|
|
1104
|
+
premiseTrust(d.viaY1, SUBCLASS_PREDICATE, d.viaY2),
|
|
1105
|
+
]);
|
|
1106
|
+
return {
|
|
1107
|
+
subject: d.subject, predicate: SUBCLASS_PREDICATE, object: d.object,
|
|
1108
|
+
provenance: ENTAILED_SCM_SVF_PROVENANCE,
|
|
1109
|
+
// same sub-1 discount as cax-dw/cls-svf1, same reason (see CAX_DW_RULE_CONFIDENCE).
|
|
1110
|
+
...(premiseTrusts.length ? { premiseTrusts, ruleConfidence: SCM_SVF_RULE_CONFIDENCE } : {}),
|
|
1111
|
+
};
|
|
1112
|
+
}),
|
|
746
1113
|
];
|
|
747
1114
|
const { ids } = await appendFacts(repoDir, toWrite);
|
|
748
1115
|
const written = [];
|
|
@@ -763,6 +1130,10 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
763
1130
|
written.push({ id: ids[i], subject: d.subject, object: d.object, via: d.viaValue, rule: CLS_SVF1_RULE });
|
|
764
1131
|
i += 1;
|
|
765
1132
|
}
|
|
1133
|
+
for (const d of scmSvfDerived) {
|
|
1134
|
+
written.push({ id: ids[i], subject: d.subject, object: d.object, via: d.viaY1, rule: SCM_SVF_RULE });
|
|
1135
|
+
i += 1;
|
|
1136
|
+
}
|
|
766
1137
|
return { derived: written, count: written.length, budget, depth, truncated: written.length >= budget };
|
|
767
1138
|
}
|
|
768
1139
|
|
package/src/toml-config.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// behaviour, byte-for-byte).
|
|
8
8
|
//
|
|
9
9
|
// Three pure entry points, consumed later by the cli:
|
|
10
|
-
// loadTomlConfig(rootDir)
|
|
10
|
+
// loadTomlConfig(rootDir,{file}) → raw parsed TOML, or null when absent
|
|
11
11
|
// normalizeConfig(raw,{configDir}) → canonical sparse shape (present keys only)
|
|
12
12
|
// mergeEffective({args,toml,defaults}) → {effective, sources} (arg>toml>default)
|
|
13
13
|
|
|
@@ -17,12 +17,13 @@ import { parse } from "smol-toml";
|
|
|
17
17
|
|
|
18
18
|
export const CONFIG_FILE = "tmct.toml";
|
|
19
19
|
|
|
20
|
-
/** Read `<rootDir>/tmct.toml`
|
|
20
|
+
/** Read `<rootDir>/tmct.toml` (or an explicit `{file}` override — `--config
|
|
21
|
+
* <path>`, a file, not a dir) and return the raw parsed table.
|
|
21
22
|
* - Absent file → `null` (the "today" signal: shipped defaults, byte-identical).
|
|
22
23
|
* - Present but unparseable → throws a clear error naming the file + parse cause
|
|
23
24
|
* (a `secret_exclude` misparse is security-relevant — never swallow it). */
|
|
24
|
-
export async function loadTomlConfig(rootDir) {
|
|
25
|
-
const file = join(rootDir, CONFIG_FILE);
|
|
25
|
+
export async function loadTomlConfig(rootDir, { file: fileOverride } = {}) {
|
|
26
|
+
const file = fileOverride || join(rootDir, CONFIG_FILE);
|
|
26
27
|
let text;
|
|
27
28
|
try {
|
|
28
29
|
text = await readFile(file, "utf8");
|
|
@@ -93,6 +94,14 @@ export async function normalizeConfig(raw, { configDir } = {}) {
|
|
|
93
94
|
if (src.graph_file !== undefined) {
|
|
94
95
|
cfg.graphFile = resolve(dir, String(src.graph_file));
|
|
95
96
|
}
|
|
97
|
+
// `graph_files` (array, multi-graph — the CLI/config unification batch):
|
|
98
|
+
// sits ALONGSIDE `graph_file`, never replaces it. A single-element array is
|
|
99
|
+
// legal but `graph_file` stays the byte-identical single-graph path most
|
|
100
|
+
// repos use; `graph_files` only matters once it names more than one file.
|
|
101
|
+
if (src.graph_files !== undefined) {
|
|
102
|
+
const arr = Array.isArray(src.graph_files) ? src.graph_files : [src.graph_files];
|
|
103
|
+
cfg.graphFiles = arr.map((p) => resolve(dir, String(p)));
|
|
104
|
+
}
|
|
96
105
|
const corpus = src.corpus || {};
|
|
97
106
|
if (corpus.tier !== undefined) cfg.corpus = { tier: corpus.tier };
|
|
98
107
|
const seed = src.seed || {};
|