@polycode-projects/the-mechanical-code-talker 1.5.4 → 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.
Files changed (46) hide show
  1. package/README.md +123 -14
  2. package/ROADMAP.md +233 -1392
  3. package/bin/tmct.mjs +479 -98
  4. package/corpus/README.md +3 -0
  5. package/corpus/generated/README.md +43 -0
  6. package/corpus/generated/ace-surface-variants.jsonl +17 -0
  7. package/corpus/generated/manifest.json +9 -0
  8. package/corpus/tier2/generate.mjs +14668 -0
  9. package/corpus/tier2/human-examples-large.jsonl +1928 -0
  10. package/corpus/tier2/human-examples-medium.jsonl +356 -0
  11. package/corpus/tier2/human-examples.jsonl +120 -0
  12. package/corpus/tier2/human-large.jsonl +12001 -0
  13. package/corpus/tier2/human-medium.jsonl +944 -0
  14. package/corpus/tier2/human.jsonl +664 -0
  15. package/corpus/tier2/manifest.json +42 -0
  16. package/package.json +14 -8
  17. package/src/answer-variants.json +47 -0
  18. package/src/answer-variants.mjs +67 -0
  19. package/src/ask-browser-entry.mjs +34 -0
  20. package/src/ask-browser.bundle.js +5095 -0
  21. package/src/ask-vocab.mjs +93 -8
  22. package/src/ask.mjs +451 -49
  23. package/src/chat.mjs +1391 -141
  24. package/src/cli-args.mjs +164 -0
  25. package/src/codegraph.mjs +170 -32
  26. package/src/completions/graph-adapter.mjs +118 -0
  27. package/src/extensions.mjs +100 -19
  28. package/src/grammar/ace.mjs +85 -3
  29. package/src/grammar/lexicon-core.json +9531 -63
  30. package/src/grammar/lexicon.mjs +58 -8
  31. package/src/graph-merge.mjs +114 -0
  32. package/src/index.mjs +14 -0
  33. package/src/init.mjs +40 -14
  34. package/src/interpret/normalize.mjs +88 -3
  35. package/src/interpret/strategies/grammar.mjs +10 -0
  36. package/src/interpret/strategies/keywords.mjs +20 -0
  37. package/src/interpret/strategies/noise-strip.mjs +73 -4
  38. package/src/memory/core.mjs +466 -8
  39. package/src/router/goal-reasoner.mjs +41 -7
  40. package/src/router/guardrail.mjs +37 -7
  41. package/src/router/resolver.mjs +50 -4
  42. package/src/sessions.mjs +5 -1
  43. package/src/source.mjs +54 -1
  44. package/src/syllogise.mjs +398 -27
  45. package/src/toml-config.mjs +13 -4
  46. 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): FOUR rules —
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 four rules:
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), or
58
- // `entailed:someValuesFrom` (cls-svf1) provenance (a first-class entailed
59
- // Source, trust prior 0.3 in memory/trust.mjs) so it is LOW trust, NEVER
60
- // outranks a stated fact, and is fully RETRACTABLE by provenance when the
61
- // source graph moves. cax-dw's and cls-svf1's conclusions additionally ride
62
- // trust.mjs's entailed hook when their OWN premises are resolvable in the
63
- // pre-pass snapshot premise-derived (`min(premiseTrusts) × ruleConfidence`),
64
- // still always strictly below its weakest premise (see syllogise()'s own doc
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 fact,
572
- * forward-chains FOUR rules — scm-sco (⊑-transitivity) then cax-sco (type
573
- * propagation, seeing THIS pass's own scm-sco conclusions too, so a fresh
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
- * and materializes each NEW conclusion via `appendFacts` with
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 + trust (PLAN_INFERENCE_TESTING.md S4
581
- * stage 2's entailed hook: `min(premiseTrusts) x ruleConfidence` when the
582
- * conclusion's OWN premises are resolvable in the pre-pass snapshot, falling
583
- * back to the bare entailed prior memory/trust.mjs's SOURCE_PRIOR floor
584
- * when they are not, e.g. a premise itself only exists because THIS SAME
585
- * pass just derived it a round earlier; still low, still never outranks a
586
- * stated fact, just less precisely premise-derived for that one case).
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 four
591
- * rules (scm-sco, then cax-sco, then cax-dw, then cls-svf1), default 50
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 four rules), not one appendFact per derived fact — appendFacts (the
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
 
@@ -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) → raw parsed TOML, or null when absent
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` and return the raw parsed table.
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 || {};