@polycode-projects/the-mechanical-code-talker 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +441 -202
  2. package/bin/tmct.mjs +126 -1
  3. package/package.json +4 -2
  4. package/src/answer-variants.mjs +8 -36
  5. package/src/ask-browser-entry.mjs +5 -23
  6. package/src/ask-browser.bundle.js +1 -2
  7. package/src/ask-nlp.mjs +9 -23
  8. package/src/ask-vocab.mjs +139 -589
  9. package/src/ask.mjs +627 -1729
  10. package/src/chat.mjs +1684 -2872
  11. package/src/cli-args.mjs +14 -28
  12. package/src/codegraph.mjs +236 -644
  13. package/src/completions/complete.mjs +18 -62
  14. package/src/completions/graph-adapter.mjs +14 -60
  15. package/src/completions/group.mjs +12 -68
  16. package/src/completions/infer.mjs +38 -126
  17. package/src/completions/prune.mjs +17 -70
  18. package/src/completions/rank.mjs +16 -69
  19. package/src/completions/search.mjs +8 -31
  20. package/src/concept.mjs +32 -88
  21. package/src/conformance.mjs +11 -15
  22. package/src/corpus/conceptnet.mjs +31 -89
  23. package/src/corpus/templates.mjs +19 -45
  24. package/src/corpus/unknown-ingest.mjs +31 -92
  25. package/src/embed.mjs +10 -22
  26. package/src/extensions.mjs +50 -154
  27. package/src/finish.mjs +35 -91
  28. package/src/grammar/ace.mjs +16 -40
  29. package/src/grammar/assert.mjs +1 -1
  30. package/src/grammar/lexicon-core.json +1 -1
  31. package/src/grammar/lexicon.mjs +9 -27
  32. package/src/graph-merge.mjs +2 -3
  33. package/src/hash.mjs +6 -14
  34. package/src/index.mjs +6 -10
  35. package/src/init.mjs +38 -125
  36. package/src/interpret/fuzzy.mjs +10 -29
  37. package/src/interpret/merge.mjs +9 -27
  38. package/src/interpret/normalize.mjs +137 -585
  39. package/src/interpret/pipeline.mjs +23 -71
  40. package/src/interpret/strategies/ace.mjs +7 -31
  41. package/src/interpret/strategies/constructions.mjs +14 -41
  42. package/src/interpret/strategies/grammar.mjs +21 -60
  43. package/src/interpret/strategies/keywords.mjs +42 -131
  44. package/src/interpret/strategies/noise-strip.mjs +18 -89
  45. package/src/memory/bias.mjs +11 -54
  46. package/src/memory/blocks.mjs +18 -69
  47. package/src/memory/core.mjs +171 -591
  48. package/src/memory/fold.mjs +0 -0
  49. package/src/memory/inspect.mjs +7 -25
  50. package/src/memory/shacl.mjs +10 -39
  51. package/src/memory/trust.mjs +26 -127
  52. package/src/memory-ask-browser-entry.mjs +7 -30
  53. package/src/memory-ask-browser.bundle.js +1 -1
  54. package/src/paraphrase.mjs +20 -53
  55. package/src/planning.mjs +15 -157
  56. package/src/prose-nlp.mjs +4 -17
  57. package/src/prose.mjs +19 -67
  58. package/src/providers/bootstrap.mjs +1 -2
  59. package/src/providers/fixture.mjs +1 -2
  60. package/src/providers/graph-service.mjs +28 -59
  61. package/src/repository-interface.mjs +6 -8
  62. package/src/router/drive.mjs +183 -0
  63. package/src/router/goal-reasoner.mjs +66 -231
  64. package/src/router/guardrail.mjs +20 -58
  65. package/src/router/planner.mjs +15 -46
  66. package/src/router/registry.mjs +13 -43
  67. package/src/router/resolver.mjs +46 -131
  68. package/src/router/results.mjs +231 -0
  69. package/src/schema-docs.mjs +10 -27
  70. package/src/server-http.mjs +10 -19
  71. package/src/server.mjs +22 -28
  72. package/src/sessions.mjs +15 -30
  73. package/src/source-slice.mjs +5 -7
  74. package/src/source.mjs +10 -20
  75. package/src/syllogise.mjs +187 -575
  76. package/src/telemetry.mjs +3 -3
  77. package/src/toml-config.mjs +4 -4
  78. package/src/tui/app.mjs +9 -19
  79. package/src/viz.mjs +66 -123
  80. package/src/wink-model.mjs +10 -24
package/src/syllogise.mjs CHANGED
@@ -1,105 +1,28 @@
1
- // syllogise.mjs — tmct's speculative-inference engine (ROADMAP Phase 9 /
2
- // archive/PLAN_SPECULATIVE_INFERENCE.md, growing toward tier-5 "the Syllogist"
3
- // per PLAN_INFERENCE_TESTING.md). Offline, $0, deterministic, HARD-bounded idle
4
- // CPU: forward-chain entailments over the OWL-labelled memory graph so a future
5
- // query-time MISS becomes a lookup. The MATERIALIZING pass (`syllogise()`,
6
- // below) is NEVER on the chat hot path — it only ever runs as the explicit
7
- // `npx tmct syllogise` batch job. The PURE kernels it's built from
8
- // (`deriveSubClassClosure`/`deriveTypePropagation`) are plain, I/O-free
9
- // functions, so a caller may also reuse them for a small, bounded, READ-ONLY
10
- // live check (e.g. chat.mjs's ISA proof chase, PLAN_INFERENCE_TESTING.md
11
- // INF-A2) without that being "the batch pass on the hot path" — nothing is
12
- // written to memory unless `syllogise()` itself is called.
1
+ // syllogise.mjs — tmct's speculative-inference engine, growing toward
2
+ // tier-5 "the Syllogist". Offline, deterministic: forward-chains entailments
3
+ // over the OWL-labelled memory graph so a future query-time MISS becomes a
4
+ // lookup. `syllogise()` (the materializing pass) only runs as the explicit
5
+ // `npx tmct syllogise` batch job; the pure kernels below may also be reused
6
+ // for a small, bounded, read-only live check without writing anything.
13
7
  //
14
- // Deliberately narrow (the plan's kill-criterion discipline): FIVE rules —
15
- // - scm-sco: rdfs:subClassOf transitivity — (a ⊑ b), (b ⊑ c) ⊨ (a ⊑ c).
16
- // The simplest OWL 2 RL rule, exactly what the ACE grammar's pattern 1
17
- // emits (`deriveSubClassClosure`).
18
- // - cax-sco: rdf:type propagation across a subClassOf chain (x rdf:type
19
- // C), (C D) (x rdf:type D). The OWL 2 RL class-axiom rule that
20
- // completes scm-sco for INSTANCE membership (`deriveTypePropagation`).
21
- // - cax-dw: disjointness violation — (x rdf:type C1), (C1 owl:disjointWith
22
- // C2) ⊨ x is NOT of type C2, checked over C1's FULL ⊑-ancestor closure
23
- // (the ⊑-lift, PLAN_INFERENCE_TESTING.md §1 footnote²) — the first rule on
24
- // this ladder to produce a PROVABLE "no" rather than only "yes"/"unproven"
25
- // (`deriveDisjointViolations`, PLAN_INFERENCE_TESTING.md §4 stage 3).
26
- // - cls-svf1: someValuesFrom application — (x P y), (y rdf:type C2), (R
27
- // owl:onProperty P), (R owl:someValuesFrom C2) ⊨ (x rdf:type R) — anyone
28
- // who P's something of type C2 is of type R, the restriction CLASS
29
- // itself (OWL 2 RL Table 8's cls-svf1, W3C OWL 2 RL profile). Joins over
30
- // exactly the triple shape the ACE grammar's pattern 4 ("every N1 that
31
- // VERBs a N2 is a N3") already emits for its restriction node `R` (`R
32
- // rdf:type owl:Restriction`, `R owl:onProperty P`, `R owl:someValuesFrom
33
- // N2` — `src/grammar/ace.mjs`'s `parseRestriction`) — no representational
34
- // gap, this rule was simply never implemented. Deliberately scoped to
35
- // JUST cls-svf1 (the restriction-membership half): it does NOT chase the
36
- // further `owl:intersectionOf`/cls-int1 step pattern 4 ALSO emits (`(N1 ⊓
37
- // R) ⊑ N3`) — concluding the ORIGINAL worked example's "chat.mjs is a
38
- // suite" needs x typed in BOTH N1 and R, then intersection-membership,
39
- // then cax-sco across `⊑ N3`, three more rules deep. That composition is
40
- // a documented follow-up (PLAN_INFERENCE_TESTING.md §4 stage 4's
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`).
8
+ // Five rules:
9
+ // - scm-sco: (a ⊑ b), (b ⊑ c) ⊨ (a ⊑ c) — subClassOf transitivity (`deriveSubClassClosure`).
10
+ // - cax-sco: (x rdf:type C), (C D) (x rdf:type D) — type propagates across a ⊑ chain (`deriveTypePropagation`).
11
+ // - cax-dw: (x rdf:type C1), (C1 owl:disjointWith C2) ⊨ x is NOT of type C2, checked over C1's full ⊑-ancestor closure — the first rule that proves a "no" (`deriveDisjointViolations`).
12
+ // - cls-svf1: (x P y), (y rdf:type C2), (R owl:onProperty P), (R owl:someValuesFrom C2) ⊨ (x rdf:type R) — someValuesFrom application; stops at restriction membership, doesn't chase the further intersectionOf/cls-int1 step (`deriveSomeValuesFromApplication`).
13
+ // - scm-svf1: two independently declared someValuesFrom restrictions over the same property, with ⊑-related filler classes, entail the restriction nodes are themselves ⊑-related (`deriveSomeValuesFromSubsumption`).
54
14
  //
55
- // The whole safety story is four guards, all mechanical, shared by all five rules:
56
- // - BUDGET at most `budget` NEW derivations per pass (default 50), and at
57
- // most `depth` fixpoint rounds (scm-sco onlycax-sco/cax-dw need no
58
- // fixpoint, see their own doc comments). The pass stops at whichever bites
59
- // first, deterministically (candidates are sorted before truncation).
60
- // - FOCUS when a focus set is given (the fold hands us the just-touched class
61
- // footprint), a derivation is admitted only if it touches focus (a, the pivot
62
- // b, or c ∈ focus — "focus-connected, one step out"). No focus ⇒ whole graph
63
- // (the explicit `npx tmct syllogise` batch pass).
64
- // - SCREENS — tautology (a ⊑ a / x:x is never written) and dedup (a derivation
65
- // that already exists, stated OR previously entailed, is skipped via its
66
- // content key — the same novelty test appendFact's content-hash id enforces).
15
+ // Four safety guards, shared by all five: BUDGET (max new derivations per
16
+ // pass, and max fixpoint rounds for scm-sco); FOCUS (an optional touched-
17
+ // class footprint scopes a derivation to what's relevant no focus means
18
+ // whole-graph); SCREENS (tautology and dedup against facts already stated or
19
+ // entailed); and TRUST (every derived fact writes under a first-class
20
+ // `entailed:*` Source, prior 0.3, so it never outranks a stated fact and is
21
+ // retractable by provenance).
67
22
  //
68
- // Every derived fact is written via appendFacts with `entailed:subClassOf`
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
78
- // comment) — rather than the bare entailed floor; scm-sco/cax-sco do not
79
- // (yet) engage that hook — see syllogise()'s doc comment for why that
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`).
23
+ // Two further capabilities below are LIVE-CHASE ONLY, never part of the
24
+ // batch pass: cardinality monotonicity (`proveCardinalityAtLeast`) and
25
+ // max-cardinality-0 as encoded negation (`proveMaxCardinalityZeroDenial`).
103
26
 
104
27
  import { loadMemory, appendFacts, readFactRows, normFactTerm, factIdForTriple, removeFacts } from "./memory/core.mjs";
105
28
 
@@ -110,52 +33,30 @@ export const SYLLOGISE_RULE = "subClassOf";
110
33
  export const ENTAILED_PROVENANCE = `entailed:${SYLLOGISE_RULE}`;
111
34
 
112
35
  /** cax-sco: the type-propagation rule, and the provenance tag its conclusions
113
- * carry (PLAN_INFERENCE_TESTING.md §4 stage 1). */
36
+ * carry. */
114
37
  export const TYPE_PREDICATE = "rdf:type";
115
38
  export const CAX_SCO_RULE = "type";
116
39
  export const ENTAILED_TYPE_PROVENANCE = `entailed:${CAX_SCO_RULE}`;
117
40
 
118
- /** cax-dw: x rdf:type C1, C1 owl:disjointWith C2 |= x is NOT of type C2 - a
119
- * provable "no", never a guessed one (PLAN_INFERENCE_TESTING.md S4 stage 3,
120
- * INF-B1). The conclusion is materialized on the SAME owl:disjointWith
121
- * predicate the ACE grammar's pattern 6 ("no N1 is a N2") already emits
122
- * class-to-class - chat.mjs's FACT_PREDICATE_PHRASES already renders it "is
123
- * not a" for either shape, so an instance-level disjointWith fact reads
124
- * correctly ("redis.mjs is not a queue") with no new phrase table entry
125
- * needed. */
41
+ /** cax-dw: x rdf:type C1, C1 owl:disjointWith C2 |= x is NOT of type C2 a
42
+ * provable "no", never a guessed one. */
126
43
  export const DISJOINT_PREDICATE = "owl:disjointWith";
127
44
  export const CAX_DW_RULE = "disjointWith";
128
45
  export const ENTAILED_DISJOINT_PROVENANCE = `entailed:${CAX_DW_RULE}`;
129
- /** trust.mjs's entailed hook's rule-confidence for cax-dw, deliberately < 1
130
- * (see syllogise()'s toWrite mapping): keeps a premise-derived conclusion
131
- * STRICTLY below its weakest premise's trust, every time, honouring this
132
- * module's "never outranks a stated fact" invariant while still riding well
133
- * above the bare entailed prior (memory/trust.mjs SOURCE_PRIOR.entailed). */
46
+ /** Rule-confidence < 1 so a premise-derived conclusion stays strictly below
47
+ * its weakest premise's trust, never a stated fact's equal. */
134
48
  export const CAX_DW_RULE_CONFIDENCE = 0.95;
135
49
 
136
50
  /** cls-svf1: x P y, y rdf:type C2, R owl:onProperty P, R owl:someValuesFrom
137
- * C2 |= x rdf:type R anyone who P's something of type C2 is of type R,
138
- * the restriction CLASS itself (OWL 2 RL Table 8's cls-svf1, PLAN_INFERENCE
139
- * _TESTING.md S1 INF-B2, S4 stage 4). Joins over the SAME `owl:Restriction`/
140
- * `owl:onProperty`/`owl:someValuesFrom` triple shape the ACE grammar's
141
- * pattern 4 already emits for "every N1 that VERBs a N2 is a N3"
142
- * (`src/grammar/ace.mjs`'s `parseRestriction`) - no new representational
143
- * work, this rule simply consumes triples the grammar was already writing.
144
- * `owl:onProperty`'s object and a taught property edge's PREDICATE are
145
- * stored in two different casings today (a stored fact's predicate keeps
146
- * its raw vocabulary spelling, e.g. "tmct:imports"; a triple's OBJECT slot -
147
- * which is what `owl:onProperty`'s value occupies - is normFactTerm'd down
148
- * to "imports" at write time, `memory/core.mjs` `appendFact`) - the pure
149
- * kernel below normFactTerm's the raw predicate itself before comparing, so
150
- * both sides converge on the same spelling without a special case. */
51
+ * C2 |= x rdf:type R (OWL 2 RL Table 8). `owl:onProperty`'s stored value and
52
+ * a taught property edge's predicate differ in casing (normFactTerm'd vs.
53
+ * raw vocabulary spelling), so the kernel below normalizes both before
54
+ * comparing. */
151
55
  export const ON_PROPERTY_PREDICATE = "owl:onProperty";
152
56
  export const SOME_VALUES_FROM_PREDICATE = "owl:someValuesFrom";
153
57
  export const CLS_SVF1_RULE = "someValuesFrom";
154
58
  export const ENTAILED_SVF1_PROVENANCE = `entailed:${CLS_SVF1_RULE}`;
155
- /** trust.mjs's entailed hook's rule-confidence for cls-svf1 - the same
156
- * sub-1 discount cax-dw uses and for the identical reason (see
157
- * CAX_DW_RULE_CONFIDENCE's own comment): keeps a premise-derived conclusion
158
- * STRICTLY below its weakest premise's trust, every time. */
59
+ /** Same sub-1 discount as CAX_DW_RULE_CONFIDENCE, same reason. */
159
60
  export const CLS_SVF1_RULE_CONFIDENCE = 0.95;
160
61
 
161
62
  const SEP = "␟"; // an in-key separator no fact term can contain
@@ -174,27 +75,13 @@ const RESERVED_PREDICATES = new Set([
174
75
  "rdfs:subclassof", "rdf:type", "owl:disjointwith", "owl:onproperty", "owl:somevaluesfrom", "owl:intersectionof",
175
76
  ]);
176
77
 
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
- */
78
+ /** PURE `min(premiseTrusts) × ruleConfidence`, clamped to [0,1] — for a live,
79
+ * read-only chase with no Fact to hand to appendFacts/computeTrust. Returns
80
+ * `null` when no numeric premise trust was supplied. */
191
81
  export function entailedTrustFrom(premiseTrusts, ruleConfidence = 1) {
192
82
  const nums = (Array.isArray(premiseTrusts) ? premiseTrusts : []).filter((t) => typeof t === "number");
193
83
  if (!nums.length) return null;
194
84
  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
85
  return Number(clamped.toFixed(6));
199
86
  }
200
87
 
@@ -274,8 +161,8 @@ export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus =
274
161
  * call, cached per class so repeated queries against the same edge set never
275
162
  * re-walk. Factored out so every rule that needs "the whole taught ⊑-chain a
276
163
  * class sits in" (`deriveTypePropagation`'s cax-sco, `deriveDisjointViolations`'s
277
- * cax-dw ⊑-lift, PLAN_INFERENCE_TESTING.md §1 footnote²) shares ONE closure
278
- * walk instead of three near-identical ones. Pure, no I/O.
164
+ * cax-dw ⊑-lift) shares ONE closure walk instead of three near-identical ones.
165
+ * Pure, no I/O.
279
166
  */
280
167
  function buildAncestorCloser(subClassEdges) {
281
168
  const succ = new Map(); // class -> Set(direct superclass): the subClassOf relation to close
@@ -302,18 +189,11 @@ function buildAncestorCloser(subClassEdges) {
302
189
 
303
190
  /**
304
191
  * PURE cax-sco: rdf:type propagation across a subClassOf chain — (x rdf:type
305
- * C), (C ⊑ … ⊑ D) ⊨ (x rdf:type D). `typeEdges` ([[x,C], …]) and
306
- * `subClassEdges` ([[a,b], …]) are both already-normalized term pairs —
307
- * exactly `deriveSubClassClosure`'s edge shape, just filtered by predicate.
308
- * `subClassEdges` is closed HERE (a full ancestor walk per class, memoized
309
- * `buildAncestorCloser`, above), so a single call sees the WHOLE taught
310
- * ⊑-chain a class sits in, not just its immediate superclass — no fixpoint
311
- * rounds are needed (unlike `deriveSubClassClosure`, which must re-scan as
312
- * ITS OWN relation grows; `subClassEdges` here is a fixed input, never
313
- * mutated by this function).
314
- * Returns ONLY new `{ subject, object, via }` conclusions (`via` = the
315
- * subject's directly-taught type), bounded by `budget`, focus-filtered,
316
- * tautology- and dedup-screened, deterministic order. No I/O.
192
+ * C), (C ⊑ … ⊑ D) ⊨ (x rdf:type D). `subClassEdges` is a fixed input (unlike
193
+ * `deriveSubClassClosure`'s own growing relation), so one ancestor walk per
194
+ * class (`buildAncestorCloser`) covers the whole chain no fixpoint rounds
195
+ * needed. Returns ONLY new `{ subject, object, via }` conclusions, bounded by
196
+ * `budget`, focus-filtered, tautology- and dedup-screened, deterministic order.
317
197
  */
318
198
  export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, focus = null } = {}) {
319
199
  const ancestorsOf = buildAncestorCloser(subClassEdges);
@@ -351,42 +231,22 @@ export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, f
351
231
  }
352
232
 
353
233
  /**
354
- * PURE cax-dw: x rdf:type C1, C1 owl:disjointWith C2 |= x is NOT of type C2 -
355
- * a provable "no" (PLAN_INFERENCE_TESTING.md S1 INF-B1, S4 stage 3), never a
356
- * guessed one: a pair this rule cannot connect through a stated disjointness
357
- * is simply not returned (never asserted "no" by absence - that "cannot be
358
- * proven" answer shape is the CALLER's job, chat.mjs, not this kernel's).
359
- *
360
- * Includes the S1 footnote2 lift ("B1's hardest cell"): x in mock, mock ⊑
361
- * fixture, fixture disjointWith test |= x not-in test - disjointness is
362
- * checked over `c`'s FULL ⊑-ancestor closure (`buildAncestorCloser`, shared
363
- * with `deriveTypePropagation` - reused, not reimplemented), not merely `x`'s
364
- * direct stated type. `disjointEdges` ([[c1,c2], …]) is the OWL-symmetric
365
- * relation as taught ("no cache is a queue" stores one directed [cache,
366
- * queue] row - disjointness has no preferred direction, so both orderings
367
- * are treated as the same fact here) and also doubles as this rule's OWN
368
- * idempotency ledger: a caller that re-feeds a prior pass's own entailed
369
- * instance-level disjointWith rows back in through `disjointEdges` (exactly
370
- * how `syllogise()` re-reads `readFactRows` every call) gets them skipped by
371
- * the same dedup/novelty screen below, with no separate "already derived"
372
- * bookkeeping needed.
373
- *
374
- * Returns ONLY new `{ subject, object, viaType, viaClass }` conclusions -
375
- * `viaType` is `x`'s directly-taught type, `viaClass` is the specific class
376
- * in that type's ⊑-closure (itself, for a direct hit, or an ancestor, for the
377
- * lift) the disjointness was actually asserted against - bounded by `budget`,
378
- * focus-filtered, tautology- and dedup-screened, deterministic order. No I/O.
234
+ * PURE cax-dw: x rdf:type C1, C1 owl:disjointWith C2 |= x is NOT of type C2
235
+ * a provable "no", never a guessed one; a pair this rule can't connect is
236
+ * simply not returned. Includes the ⊑-lift: disjointness is checked over
237
+ * `c`'s FULL ⊑-ancestor closure, not merely `x`'s direct stated type.
238
+ * `disjointEdges` is the OWL-symmetric relation as taught (one directed row
239
+ * covers both orderings). Returns ONLY new `{ subject, object, viaType,
240
+ * viaClass }` conclusions, bounded by `budget`, focus-filtered, tautology-
241
+ * and dedup-screened, deterministic order.
379
242
  */
380
243
  export function deriveDisjointViolations(typeEdges, subClassEdges, disjointEdges, { budget = 50, focus = null } = {}) {
381
244
  const ancestorsOf = buildAncestorCloser(subClassEdges);
382
245
 
383
- // disjointWith is symmetric (OWL semantics: C1 disjointWith C2 |= C2
384
- // disjointWith C1), so both directions of every taught/prior-entailed pair
385
- // are indexed. `presentPairs` doubles as the dedup/novelty screen (see
386
- // above) - it is deliberately NOT split by "class-class taught" vs
387
- // "instance-class entailed": the predicate is the same either way, and a
388
- // synthetic individual term never collides with a class-noun term in this
389
- // domain (CODE_REF individuals always contain one of `. / \ # : @`).
246
+ // disjointWith is symmetric, so both directions of every pair are indexed.
247
+ // A synthetic individual term never collides with a class-noun term here
248
+ // (CODE_REF individuals always contain one of `. / \ # : @`), so class- and
249
+ // instance-level pairs share one map safely.
390
250
  const disjointOf = new Map(); // term -> Set(disjoint partner terms)
391
251
  const presentPairs = new Set(); // "a\0b" for every disjointWith row already known (either order)
392
252
  for (const [a, b] of disjointEdges || []) {
@@ -411,7 +271,7 @@ export function deriveDisjointViolations(typeEdges, subClassEdges, disjointEdges
411
271
  if (seenTypeEdge.has(tk)) continue;
412
272
  seenTypeEdge.add(tk);
413
273
  // the ⊑-lift: x's own class closure is {c} ∪ ancestorsOf(c) - a direct
414
- // hit needs no lift (d === c), the S1 footnote2 case needs one hop or more.
274
+ // hit needs no lift (d === c), an inherited one needs one hop or more.
415
275
  for (const d of [c, ...ancestorsOf(c)]) {
416
276
  const partners = disjointOf.get(d);
417
277
  if (!partners) continue;
@@ -438,41 +298,15 @@ export function deriveDisjointViolations(typeEdges, subClassEdges, disjointEdges
438
298
 
439
299
  /**
440
300
  * PURE cls-svf1: x P y, y rdf:type C2, R owl:onProperty P, R
441
- * owl:someValuesFrom C2 |= x rdf:type R (OWL 2 RL Table 8's cls-svf1,
442
- * PLAN_INFERENCE_TESTING.md S1 INF-B2, S4 stage 4) - see this module's
443
- * header comment for the deliberate scope line (stops at restriction
444
- * membership, does not chase the further owl:intersectionOf/cls-int1 step
445
- * pattern 4 also emits).
446
- *
447
- * `propertyEdges` ([[x, predicate, y], …], `predicate` the RAW, un-normalized
448
- * vocabulary spelling exactly as a stored Fact's predicate reads, e.g.
449
- * "tmct:imports") is every taught/prior-entailed object-property assertion —
450
- * `syllogise()` builds this from every stored row whose predicate is NOT one
451
- * of the other three rules' reserved predicates (RESERVED_PREDICATES, above),
452
- * so ANY declared object property is a candidate, not just one hard-coded
453
- * verb. `typeEdges`/`subClassEdges` are the same shape `deriveTypePropagation`/
454
- * `deriveDisjointViolations` take (already-normalized [x,C] / [a,b] pairs);
455
- * `y`'s type is lifted through its FULL ⊑-ancestor closure (`buildAncestorCloser`,
456
- * shared machinery, same ⊑-lift discipline as cax-dw's own footnote2 case) so
301
+ * owl:someValuesFrom C2 |= x rdf:type R (OWL 2 RL Table 8). `propertyEdges`
302
+ * is every taught/prior-entailed object-property assertion (raw predicate
303
+ * spelling); `restrictionEdges` is each restriction node's (property, target)
304
+ * declaration. `y`'s type is lifted through its full ⊑-ancestor closure, so
457
305
  * "y is a mock" still satisfies a restriction declared over "fixture" when
458
- * mock⊑fixture is taught. `restrictionEdges` ([{ restriction, property,
459
- * target }, …], `property`/`target` already normFactTerm-normalized — the
460
- * spelling `owl:onProperty`/`owl:someValuesFrom` rows store their OBJECT in)
461
- * is every restriction node's (P, C2) declaration, reconstructed by joining
462
- * a restriction's `owl:onProperty` row with its `owl:someValuesFrom` row on
463
- * the restriction's own subject — exactly `syllogise()`'s own join, exposed
464
- * here as a plain parameter so the pure kernel stays I/O-free and unit-
465
- * testable without a memory store.
466
- *
467
- * Returns ONLY new `{ subject, object, viaProperty, viaPropertyKey, viaValue,
468
- * viaType, viaTarget }` conclusions — `object` is the restriction node R
469
- * itself (the newly-entailed rdf:type value), `viaProperty` the RAW predicate
470
- * matched, `viaPropertyKey` its normalized form (R's own `owl:onProperty`
471
- * value), `viaValue` the property's object `y`, `viaType` the specific class
472
- * `y` was directly taught as, `viaTarget` the class in that type's
473
- * ⊑-closure the restriction was actually declared against (itself, for a
474
- * direct hit, or an ancestor, for the lift) — bounded by `budget`,
475
- * focus-filtered, tautology- and dedup-screened, deterministic order. No I/O.
306
+ * mock⊑fixture is taught. Returns ONLY new `{ subject, object, viaProperty,
307
+ * viaPropertyKey, viaValue, viaType, viaTarget }` conclusions (`object` is
308
+ * the restriction node R), bounded by `budget`, focus-filtered, tautology-
309
+ * and dedup-screened, deterministic order.
476
310
  */
477
311
  export function deriveSomeValuesFromApplication(propertyEdges, typeEdges, subClassEdges, restrictionEdges, { budget = 50, focus = null } = {}) {
478
312
  const ancestorsOf = buildAncestorCloser(subClassEdges);
@@ -543,34 +377,20 @@ export function deriveSomeValuesFromApplication(propertyEdges, typeEdges, subCla
543
377
  }
544
378
 
545
379
  // ---- 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.
380
+ // "every N1 has exactly n N2s" stores `{N1, rdfs:subClassOf, r}` plus the
381
+ // restriction node r's own scaffolding rows (owl:onProperty/cardinality
382
+ // kind/owl:onClass) reconstructed here the same way
383
+ // deriveSomeValuesFromApplication reconstructs someValuesFrom restrictions.
384
+ const HAS_PROPERTY_KEY = "has"; // synthetic marker parseCardinality always mints, never a real taught verb
555
385
  const CARDINALITY_KIND_OF = { "owl:cardinality": "exactly", "owl:mincardinality": "min", "owl:maxcardinality": "max" };
556
386
  export const ON_CLASS_PREDICATE = "owl:onClass";
557
387
 
558
388
  /** 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. */
389
+ * rows touching a restriction node. A restriction is only admitted when its
390
+ * own `owl:onProperty` row resolves to `HAS_PROPERTY_KEY` — keeps a
391
+ * someValuesFrom restriction's scaffolding (which also uses `owl:onProperty`,
392
+ * with a real verb) from being mistaken for a cardinality restriction.
393
+ * Returns `[{ restriction, kind, n, onClass }, …]`, sorted by restriction id. */
574
394
  export function buildCardinalityRestrictions(rows) {
575
395
  const onPropertyOf = new Map(); // restriction -> owl:onProperty's object
576
396
  const kindOf = new Map(); // restriction -> { kind, n }
@@ -597,43 +417,21 @@ export function buildCardinalityRestrictions(rows) {
597
417
  return restrictions;
598
418
  }
599
419
 
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) ----
420
+ // ---- scm-svf1: someValuesFrom restriction subsumption (W3C OWL 2 RL Table 9) ----
603
421
  export const SCM_SVF_RULE = "someValuesFromSubsumption";
604
422
  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. */
423
+ /** Same sub-1 discount as CAX_DW_RULE_CONFIDENCE, same reason. */
611
424
  export const SCM_SVF_RULE_CONFIDENCE = 0.95;
612
425
 
613
426
  /**
614
427
  * 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.
428
+ * c2 onProperty p, y1 ⊑ y2 (lifted through y1's full ⊑-ancestor closure) |=
429
+ * c1 ⊑ c2 — a schema-level fact about the restriction NODES themselves,
430
+ * requiring two independently-declared restrictions over the SAME property
431
+ * to compare. Returns ONLY new `{ subject, object, viaY1, viaY2 }`
432
+ * conclusions (the two restriction node ids, and the filler classes whose
433
+ * ⊑-relation licensed it), bounded by `budget`, focus-filtered, tautology-
434
+ * and dedup-screened, deterministic order.
637
435
  */
638
436
  export function deriveSomeValuesFromSubsumption(restrictionEdges, subClassEdges, { budget = 50, focus = null } = {}) {
639
437
  const ancestorsOf = buildAncestorCloser(subClassEdges);
@@ -680,22 +478,14 @@ export function deriveSomeValuesFromSubsumption(restrictionEdges, subClassEdges,
680
478
  }
681
479
 
682
480
  // ---- 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. */
481
+ // single-premise-sufficient a class's OWN declared cardinality restriction,
482
+ // walked through its FULL ⊑-ancestor closure. ----
483
+
484
+ /** Shared bounded proof search: walks `subject`'s own ⊑-ancestor closure
485
+ * looking for a class with a directly declared cardinality restriction
486
+ * satisfying `matches(record)`. Returns the first `{ viaClass,
487
+ * viaRestriction, record }` found (deterministic) or null. `budget` bounds
488
+ * how many candidate classes are examined. */
699
489
  function findOwnCardinalityRestriction(subClassEdges, cardinalityRestrictionEdges, subject, matches, { budget = 20, focus = null } = {}) {
700
490
  if (!subject) return null;
701
491
  const ancestorsOf = buildAncestorCloser(subClassEdges);
@@ -721,40 +511,23 @@ function findOwnCardinalityRestriction(subClassEdges, cardinalityRestrictionEdge
721
511
  return null;
722
512
  }
723
513
 
724
- // ---- cardinality monotonicity (confirmed OUTSIDE OWL 2 RL's own decidable
725
- // profile, see this file's header comment) ----
514
+ // ---- cardinality monotonicity (outside OWL 2 RL's own decidable profile) ----
726
515
  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. */
516
+ /** Same sub-1 discount as CAX_DW_RULE_CONFIDENCE. No `syllogise()` call site
517
+ * (this rule is query-rooted, never an enumerable Fact) defined anyway so
518
+ * chat.mjs's live proof chase can attach an auditable confidence figure
519
+ * (`entailedTrustFrom`, below) to its own answer. */
740
520
  export const CARDINALITY_RULE_CONFIDENCE = 0.95;
741
521
 
742
522
  /**
743
523
  * 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).
524
+ * (kind ∈ {exactly,min}, n, onClass — lifted through its full ⊑-ancestor
525
+ * closure) and a QUERIED (`onClass`, `m`), proves "`subject` has at least `m`
526
+ * `onClass`" whenever `onClass` matches and `n m`. Query-rooted, not a
527
+ * batch derivation `m` is query-specific.
753
528
  *
754
529
  * 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.
530
+ * viaRestriction }` or null.
758
531
  */
759
532
  export function proveCardinalityAtLeast(subClassEdges, cardinalityRestrictionEdges, subject, onClass, m, opts = {}) {
760
533
  if (!onClass || !Number.isFinite(m)) return null;
@@ -772,31 +545,19 @@ export function proveCardinalityAtLeast(subClassEdges, cardinalityRestrictionEdg
772
545
  // ladder's "produces a provable no" naming convention, same epistemic status
773
546
  // as cax-dw) ----
774
547
  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). */
548
+ /** Same sub-1 discount and query-rooted caveat as CARDINALITY_RULE_CONFIDENCE. */
782
549
  export const CAX_MAXC0_RULE_CONFIDENCE = 0.95;
783
550
 
784
551
  /**
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).
552
+ * PURE: `subject` ⊑ r (lifted through its full ⊑-ancestor closure), r a
553
+ * maxCardinality-0 restriction (property `has`, onClass `onClass`) |= "no
554
+ * `subject` has a `onClass`" — a universal-generalization bridge from
555
+ * `cls-maxc1`'s per-individual ABox contradiction to a class-level provable
556
+ * negative: since no witness can exist without contradiction, the general
557
+ * "no" is sound. Never infers "no" from absence no declared restriction
558
+ * simply returns null.
796
559
  *
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.
560
+ * Returns `{ subject, object: onClass, viaClass, viaRestriction }` or null.
800
561
  */
801
562
  export function proveMaxCardinalityZeroDenial(subClassEdges, cardinalityRestrictionEdges, subject, onClass, opts = {}) {
802
563
  if (!onClass) return null;
@@ -809,26 +570,17 @@ export function proveMaxCardinalityZeroDenial(subClassEdges, cardinalityRestrict
809
570
  }
810
571
 
811
572
  /**
812
- * PURE consistency checker (PLAN_INFERENCE_TESTING.md S1 INF-C2, S4 stage 5):
813
- * detects when a SINGLE subject's own already-asserted types contradict each
814
- * other — x rdf:type C1, x rdf:type C2, C1 owl:disjointWith C2 (checked over
815
- * BOTH types' FULL ⊑-ancestor closures, the same lift `deriveDisjointViolations`
816
- * uses) a REFUSE-worthy clash, not a "no" to derive and move on from. This is
817
- * a fundamentally different shape than cax-dw: cax-dw asks "can I derive a NO
818
- * for an UNASSERTED type"; this asks "do X's OWN taught/entailed types already
819
- * clash with each other" — every stored belief about a contradictory subject is
820
- * suspect, not just the one pair being queried, so the caller's job (chat.mjs)
821
- * is to REFUSE to answer from that subject's memory at all, not to keep
822
- * answering everything except the one clashing pair.
573
+ * PURE consistency checker: detects when a SINGLE subject's own already-
574
+ * asserted types contradict each other x rdf:type C1, x rdf:type C2, C1
575
+ * owl:disjointWith C2 (checked over both types' full ⊑-ancestor closures)
576
+ * a refuse-worthy clash, not a "no" to derive and move on from: unlike cax-dw
577
+ * (deriving a no for an unasserted type), every stored belief about a
578
+ * contradictory subject is suspect, so the caller should refuse to answer
579
+ * from that subject's memory at all.
823
580
  *
824
581
  * Returns ONLY the clashes found — `{ subject, classA, classB, viaA, viaB }`
825
- * (`viaA`/`viaB` are the specific taught types whose ⊑-closures actually
826
- * licensed the clash itself, for a direct hit, or an ancestor, for the
827
- * lift) — bounded by `budget`, focus-filtered, deterministic order, DEDUPED
828
- * so a subject with N mutually-clashing types reports each unordered pair
829
- * once. No I/O; nothing is written — same read-only discipline as
830
- * `deriveDisjointViolations`'s own live chat-side use (chat.mjs's INF-B1
831
- * cax-dw chase).
582
+ * — bounded by `budget`, focus-filtered, deduped so a subject with N
583
+ * mutually-clashing types reports each unordered pair once.
832
584
  */
833
585
  export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdges, { budget = 50, focus = null } = {}) {
834
586
  const ancestorsOf = buildAncestorCloser(subClassEdges);
@@ -891,41 +643,21 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
891
643
  }
892
644
 
893
645
  /**
894
- * Run one bounded speculative pass over the memory graph under `repoDir`
895
- * (the repo dir whose .tmct/memory/graph.json appendFact/loadMemory manage).
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
901
- * call) then cax-dw (disjointness violations, seeing THIS pass's own scm-sco
902
- * AND cax-sco conclusions too) then cls-svf1 (someValuesFrom restriction
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
907
- * `entailed:subClassOf`/`entailed:type`/`entailed:disjointWith`/
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).
646
+ * Run one bounded speculative pass over the memory graph under `repoDir`.
647
+ * Forward-chains the five rules in order scm-sco, cax-sco, cax-dw,
648
+ * cls-svf1, scm-svf1 each seeing the prior rules' conclusions from this
649
+ * same pass, and materializes each new conclusion via `appendFacts` with its
650
+ * `entailed:*` provenance. Trust rides the entailed hook
651
+ * (`min(premiseTrusts) x ruleConfidence`) when premises are resolvable in the
652
+ * pre-pass snapshot, else falls back to the bare entailed prior.
916
653
  *
917
- * opts:
918
- * - depth max fixpoint rounds (scm-sco chain growth), default 32
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
922
- * - focus Set|array of class terms; when given, only derivations touching
923
- * focus (subject, pivot, or object ∈ focus) are admitted. Omit for a
924
- * whole-graph batch pass.
654
+ * opts: `depth` (max fixpoint rounds, default 32), `budget` (max new
655
+ * derivations this pass, shared across all five rules, default 50), `focus`
656
+ * (Set|array of class terms scoping derivations to what touches it omit
657
+ * for a whole-graph pass).
925
658
  *
926
659
  * Returns { derived: [{ id, subject, object, via, rule }], count, budget,
927
- * depth, truncated } — `truncated` flags that the budget may have capped the
928
- * pass. Deterministic, offline, side-effects only in .tmct/memory.
660
+ * depth, truncated }.
929
661
  */
930
662
  export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null } = {}) {
931
663
  const memory = await loadMemory(repoDir);
@@ -933,13 +665,10 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
933
665
  const subClassEdges = rows.filter((r) => isSubClassOf(r.predicate)).map((r) => [r.subject, r.object]);
934
666
  const typeEdges = rows.filter((r) => isType(r.predicate)).map((r) => [r.subject, r.object]);
935
667
  const disjointEdges = rows.filter((r) => isDisjoint(r.predicate)).map((r) => [r.subject, r.object]);
936
- // cls-svf1's own join inputs: a restriction's owl:onProperty and
937
- // owl:someValuesFrom rows, keyed by the restriction's OWN subject so the
938
- // two can be paired without a second graph pass; `propertyEdges` is every
939
- // OTHER stored fact (any predicate not one of the three rules' reserved
940
- // predicates above) — a taught object-property assertion is a candidate
941
- // premise for whichever restriction (if any) was declared over its
942
- // predicate, never hard-coded to one verb.
668
+ // cls-svf1's join inputs: a restriction's onProperty/someValuesFrom rows,
669
+ // keyed by the restriction's own subject; propertyEdges is every other
670
+ // stored fact, a candidate premise for whichever restriction was declared
671
+ // over its predicate.
943
672
  const onPropertyOf = new Map(); // restriction -> owl:onProperty's (normalized) object
944
673
  const someValuesFromOf = new Map(); // restriction -> owl:someValuesFrom's (normalized) object
945
674
  const propertyEdges = []; // [[x, rawPredicate, y], …]
@@ -956,23 +685,12 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
956
685
  }
957
686
  const normalizedFocus = normalizeFocus(focus);
958
687
 
959
- // Pre-pass trust snapshot, keyed by exact (subject, predicate, object) —
960
- // the entailed hook's premiseTrusts lookup, wired for cax-dw ONLY (see the
961
- // dwDerived mapping below). scm-sco/cax-sco deliberately stay on the bare
962
- // entailed prior here: a spike wiring premiseTrusts through them the same
963
- // way broke three PINNED tests (test/syllogise.test.mjs's own "entailed
964
- // trust is low"/"never outranks its stated premise" assertions,
965
- // test/memory-fold.test.mjs's "entailed facts carry speculative trust") —
966
- // with `ruleConfidence` defaulting to 1, `min(premiseTrusts) × 1` can EQUAL
967
- // a stated premise's trust (e.g. two operator-taught 1.0 premises → 1.0),
968
- // which ties or outranks the very premise it was derived from, violating
969
- // this module's own "NEVER outranks a stated fact" invariant (its own
970
- // header comment, and the KILL CRITERION test). Fixing that for scm-sco/
971
- // cax-sco needs the same sub-1 `ruleConfidence` discount cax-dw uses below
972
- // (or an equivalent design decision) — real, but a separate, deliberate
973
- // follow-up, not a trivial addition (PLAN_INFERENCE_TESTING.md §4 stage 2
974
- // note left for the next pass). cax-dw is new — no pinned floor-trust tests
975
- // exist for it — so it is designed with the sub-1 discount from the start.
688
+ // Pre-pass trust snapshot for the entailed hook's premiseTrusts lookup,
689
+ // wired for cax-dw/cls-svf1/scm-svf1 only: with ruleConfidence defaulting
690
+ // to 1, min(premiseTrusts) x 1 can EQUAL a stated premise's trust (e.g. two
691
+ // 1.0 premises), tying or outranking the very premise it derived from —
692
+ // scm-sco/cax-sco stay on the bare entailed prior until they get the same
693
+ // sub-1 discount cax-dw uses below.
976
694
  const trustByTriple = new Map();
977
695
  for (const r of rows) trustByTriple.set(`${r.subject}${SEP}${r.predicate}${SEP}${r.object}`, r.trust);
978
696
  const premiseTrust = (s, p, o) => trustByTriple.get(`${s}${SEP}${p}${SEP}${o}`);
@@ -986,63 +704,39 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
986
704
  const caxDerived = remainingBudget > 0
987
705
  ? deriveTypePropagation(typeEdges, enlargedSubClassEdges, { budget: remainingBudget, focus: normalizedFocus })
988
706
  : [];
989
- // cax-dw sees the SAME enlarged subClassOf set (so its own ⊑-lift, S1
990
- // footnote2, reaches a chain scm-sco just grew this pass) — it does not
991
- // need the enlarged TYPE edge set too: it walks each direct type's full
992
- // ⊑-ancestor closure itself (deriveDisjointViolations' own doc comment).
707
+ // cax-dw sees the SAME enlarged subClassOf set (so its own ⊑-lift reaches a
708
+ // chain scm-sco just grew this pass) — it doesn't need the enlarged TYPE
709
+ // edge set too, since it walks each direct type's own ⊑-ancestor closure.
993
710
  const remainingBudgetDw = Math.max(0, budget - scmDerived.length - caxDerived.length);
994
711
  const dwDerived = remainingBudgetDw > 0
995
712
  ? deriveDisjointViolations(typeEdges, enlargedSubClassEdges, disjointEdges, { budget: remainingBudgetDw, focus: normalizedFocus })
996
713
  : [];
997
- // cls-svf1 sees the SAME enlarged subClassOf set (its own ⊑-lift, mirroring
998
- // cax-dw's) it deliberately does NOT see the enlarged type edge set: a
999
- // direct taught type on the property's VALUE is the common case (the B2
1000
- // worked example), and enlarging risks a same-pass cax-sco conclusion on
1001
- // `y` being consumed before a human can audit it; a documented scope line,
1002
- // not an oversight (mirrors cax-dw's own "does not need the enlarged TYPE
1003
- // edge set" choice, just for a different reason here).
714
+ // cls-svf1 sees the SAME enlarged subClassOf set (its own ⊑-lift) but NOT
715
+ // the enlarged type edge set, so a same-pass cax-sco conclusion on `y`
716
+ // can't be consumed before a human can audit it.
1004
717
  const remainingBudgetSvf1 = Math.max(0, budget - scmDerived.length - caxDerived.length - dwDerived.length);
1005
718
  const svf1Derived = remainingBudgetSvf1 > 0 && restrictionEdges.length
1006
719
  ? deriveSomeValuesFromApplication(propertyEdges, typeEdges, enlargedSubClassEdges, restrictionEdges, { budget: remainingBudgetSvf1, focus: normalizedFocus })
1007
720
  : [];
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).
721
+ // scm-svf1 reuses the SAME restrictionEdges built for cls-svf1 above
722
+ // needs at least two restrictions over one property to compare.
1013
723
  const remainingBudgetScmSvf = Math.max(0, budget - scmDerived.length - caxDerived.length - dwDerived.length - svf1Derived.length);
1014
724
  const scmSvfDerived = remainingBudgetScmSvf > 0 && restrictionEdges.length > 1
1015
725
  ? deriveSomeValuesFromSubsumption(restrictionEdges, enlargedSubClassEdges, { budget: remainingBudgetScmSvf, focus: normalizedFocus })
1016
726
  : [];
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
727
  const restrictionByRid = new Map(restrictionEdges.map((r) => [r.restriction, r]));
1022
728
 
1023
- // Batched write: ONE mutateMemory pass for the whole pass's conclusions
1024
- // (all five rules), not one appendFact per derived fact — appendFacts (the
1025
- // appendUtterances-precedent batch path, memory/core.mjs) does the same
1026
- // normalize+prose-tokenize+upsert work per fact but a SINGLE read-mutate-
1027
- // write, so a pass with many derivations no longer pays per-fact I/O.
1028
- // subject/object here are already-normalized terms straight off the stored
1029
- // graph (readFactRows/deriveSubClassClosure/deriveTypePropagation/
1030
- // deriveDisjointViolations never hand back an empty term), and predicate is
1031
- // always one of the three fixed constants above, so appendFacts never skips
1032
- // one of these — `ids` comes back exactly one-per-input, in order, safe to
1033
- // zip positionally below.
729
+ // Batched write: ONE mutateMemory pass for all five rules' conclusions via
730
+ // appendFacts, not one appendFact per derived fact.
1034
731
  const toWrite = [
1035
732
  ...scmDerived.map((d) => ({
1036
733
  subject: d.subject, predicate: SUBCLASS_PREDICATE, object: d.object,
1037
734
  provenance: ENTAILED_PROVENANCE,
1038
- // PLAN_SYLLOGIST.md §3's persisted-justification step, scm-sco only: the
1039
- // two premise fact ids THIS conclusion actually rode (a⊑b, b⊑c) — ids
1040
- // are content-addressed (factIdForTriple/memory/core.mjs), so this works
1041
- // whether the premise is a stated fact or another entailment this SAME
1042
- // pass just derived a round earlier (its id is predictable before it's
1043
- // even written). Read back by retractSubClassOf (below) to find every
1044
- // entailment a retracted premise could have supported, without a
1045
- // whole-graph re-scan.
735
+ // Persisted justification, scm-sco only: the two premise fact ids this
736
+ // conclusion rode (a⊑b, b⊑c) — content-addressed ids work even when a
737
+ // premise is itself an entailment this same pass just derived. Read
738
+ // back by retractSubClassOf (below) to find every entailment a
739
+ // retracted premise could have supported.
1046
740
  justification: [
1047
741
  factIdForTriple(d.subject, SUBCLASS_PREDICATE, d.via),
1048
742
  factIdForTriple(d.via, SUBCLASS_PREDICATE, d.object),
@@ -1067,22 +761,10 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
1067
761
  return {
1068
762
  subject: d.subject, predicate: DISJOINT_PREDICATE, object: d.object,
1069
763
  provenance: ENTAILED_DISJOINT_PROVENANCE,
1070
- // ruleConfidence < 1 (CAX_DW_RULE_CONFIDENCE) is deliberate, not a
1071
- // magic number: with the hook's default confidence of 1,
1072
- // min(premiseTrusts) × 1 can EQUAL a premise's own trust (e.g. two
1073
- // operator-taught 1.0 premises), tying/outranking the very premise it
1074
- // came from — this module's invariant is "never outranks a stated
1075
- // fact" (header comment), so cax-dw's conclusion is discounted
1076
- // strictly below its weakest premise, always, while still riding
1077
- // FAR above the bare 0.3 floor for a well-sourced premise pair.
1078
764
  ...(premiseTrusts.length ? { premiseTrusts, ruleConfidence: CAX_DW_RULE_CONFIDENCE } : {}),
1079
765
  };
1080
766
  }),
1081
767
  ...svf1Derived.map((d) => {
1082
- // the restriction's own two structural premises (owl:onProperty /
1083
- // owl:someValuesFrom rows) are stored with the restriction node as
1084
- // SUBJECT and the (already-normalized) property/target as OBJECT —
1085
- // premiseTrust's exact-triple lookup, so no extra normalization here.
1086
768
  const premiseTrusts = numericOnly([
1087
769
  premiseTrust(d.subject, d.viaProperty, d.viaValue),
1088
770
  premiseTrust(d.viaValue, TYPE_PREDICATE, d.viaType),
@@ -1100,12 +782,6 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
1100
782
  };
1101
783
  }),
1102
784
  ...scmSvfDerived.map((d) => {
1103
- // each restriction's own two structural premises (owl:onProperty /
1104
- // owl:someValuesFrom rows), for BOTH restrictions being compared, plus
1105
- // the y1⊑y2 subClassOf premise that licensed the comparison — always
1106
- // present here (unlike cax-dw/cls-svf1's optional lift premise): the
1107
- // kernel's own tautology screen guarantees viaY1 !== viaY2 for every
1108
- // derived scm-svf1 fact (`deriveSomeValuesFromSubsumption`'s doc comment).
1109
785
  const r1 = restrictionByRid.get(d.subject);
1110
786
  const r2 = restrictionByRid.get(d.object);
1111
787
  const premiseTrusts = numericOnly([
@@ -1155,72 +831,31 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
1155
831
  * taught (same (s,p,o) → same id → provenance union, appendFact's own upsert
1156
832
  * contract), is NOT purely entailed any more — `retractSubClassOf`'s cascade
1157
833
  * must never delete it just because its now-stale justification broke; the
1158
- * taught half is a real, independent reason to keep believing it (the trust-
1159
- * tier concern PLAN_SYLLOGIST.md §3 names: "must never touch a higher-trust
1160
- * taught-only derivation"). */
834
+ * taught half is a real, independent reason to keep believing it (must never
835
+ * touch a higher-trust taught-only derivation). */
1161
836
  function isPurelyEntailed(provenance) {
1162
837
  const tags = String(provenance || "").split(" | ").filter(Boolean);
1163
838
  return tags.length > 0 && tags.every((t) => t.startsWith("entailed:"));
1164
839
  }
1165
840
 
1166
841
  /**
1167
- * PLAN_SYLLOGIST.md §3's first real, scoped retraction slice: JTMS-style
1168
- * dependency-directed removal, for scm-sco ONLY (subClassOf transitivity
1169
- * this file's simplest rule, and §3's own worked example: "a premise later
1170
- * disappears, what else must be un-believed?"). Deliberately NOT the fuller
1171
- * ATMS §3 also sketches (tracking every alternate premise-SET per fact, "a
1172
- * further, NOT-currently-planned step") this tracks exactly ONE
1173
- * justification per entailed fact (the premise pair it actually rode, set at
1174
- * write time syllogise()'s own toWrite mapping, above): the JTMS-shaped
1175
- * step §3 names as missing today ("a JTMS-shaped single justification per
1176
- * fact in spirit, though not yet a persisted, walkable one").
1177
- *
1178
- * Retracting `subject ⊑ object` (a STATED or a previously-ENTAILED fact —
1179
- * either may be retracted) proceeds in bounded rounds:
1180
- * 1. Remove the named fact.
1181
- * 2. Scan stored entailed scm-sco facts (purely-entailed ones only —
1182
- * `isPurelyEntailed`, above) for any whose persisted justification cites
1183
- * an id removed so far — candidates.
1184
- * 3. VERIFY, never assume: a candidate is removed only if `subject ⊑
1185
- * object` is NO LONGER reachable over the SURVIVING subClassOf edge set
1186
- * (a full ⊑-ancestor walk, `buildAncestorCloser` — the SAME shared
1187
- * machinery `deriveTypePropagation`/`deriveDisjointViolations` already
1188
- * reuse, not reimplemented here). A fact with a SECOND, independent
1189
- * derivation path survives — a real possibility scm-sco's transitive
1190
- * closure allows (a⊑b⊑d AND a⊑c⊑d both license a⊑d) — exactly the
1191
- * failure mode a bare "delete anything citing the retracted id" JTMS
1192
- * walk gets wrong, and precisely why de Kleer's ATMS exists at all (§3's
1193
- * own citation). This VERIFY step is this slice's cheap, bounded answer
1194
- * to that known JTMS over-retraction limitation: one local graph walk
1195
- * per candidate, never a full alternate-justification enumeration.
1196
- * 4. Repeat: a fact confirmed-removed this round becomes a new cascade
1197
- * source for the next round (removing a mid-chain link can ripple).
1198
- *
1199
- * Bounded by `budget` (max facts examined+removed, default 50 — the SAME
1200
- * default every other rule in this file uses) and `depth` (max cascade
1201
- * rounds, default 32, mirroring `deriveSubClassClosure`'s own fixpoint cap).
1202
- * `truncated` flags the cascade may have been cut short before reaching a
1203
- * fixpoint (candidates still pending when budget/depth ran out) — the SAME
1204
- * honest-signal discipline `syllogise()`'s own `truncated` flag follows: a
1205
- * caller must not read a truncated cascade's survivors as "provably still
1206
- * consistent," only as "not yet shown inconsistent within budget."
842
+ * A scoped retraction slice: JTMS-style dependency-directed removal, for
843
+ * scm-sco ONLY. Retracting `subject ⊑ object` removes the fact, then cascades
844
+ * to any purely-entailed scm-sco fact whose persisted justification cites a
845
+ * removed id but each candidate is VERIFIED (re-derivable over the
846
+ * surviving subClassOf edge set, not just "cited a removed id") before it is
847
+ * actually removed, since a fact can have a second, independent derivation
848
+ * path (a⊑b⊑d AND a⊑c⊑d both license a⊑d) that a bare delete-by-justification
849
+ * walk would wrongly discard. Repeats in rounds a removed mid-chain link
850
+ * can ripple bounded by `budget` (max facts examined+removed) and `depth`
851
+ * (max cascade rounds).
1207
852
  *
1208
- * Known, DELIBERATE scope limit (not a bug): this only ever touches scm-sco's
1209
- * own entailed subClassOf facts. The other four rules (cax-sco/cax-dw/
1210
- * cls-svf1/scm-svf1) do not yet persist a justification (none call
1211
- * `factIdForTriple`/write `justification` — only `syllogise()`'s scmDerived
1212
- * mapping does, above), so a type/disjointWith/someValuesFrom conclusion that
1213
- * ALSO went stale when this same premise was retracted is not cascaded here.
1214
- * Extending justification-tracking to the other four rules is mechanical
1215
- * (each already computes a `via`/`viaX` pivot) but is a separate follow-up,
1216
- * not attempted in this slice.
853
+ * Scope limit: only scm-sco persists a justification today, so a
854
+ * type/disjointWith/someValuesFrom conclusion that also went stale is not
855
+ * cascaded here (mechanical to extend, not attempted in this slice).
1217
856
  *
1218
- * Returns { retracted, count, budget, depth, truncated, found } — `retracted`
1219
- * is every id actually removed (target first, then cascade order); `found`
1220
- * is false (nothing else meaningful) when `subject ⊑ object` was never a
1221
- * stored fact at all — an honest no-op, matching this module's "never guess"
1222
- * discipline. No I/O beyond the one `removeFacts` call (skipped entirely when
1223
- * `found` is false).
857
+ * Returns { retracted, count, budget, depth, truncated, found } — `found` is
858
+ * false when `subject object` was never a stored fact.
1224
859
  */
1225
860
  export async function retractSubClassOf(repoDir, subject, object, { budget = 50, depth = 32 } = {}) {
1226
861
  const s = normFactTerm(subject);
@@ -1236,11 +871,7 @@ export async function retractSubClassOf(repoDir, subject, object, { budget = 50,
1236
871
  // removed id's own edge is excluded from that round's walk onward.
1237
872
  const scRows = rows.filter((r) => isSubClassOf(r.predicate));
1238
873
  const edgeOf = new Map(scRows.map((r) => [r.id, [r.subject, r.object]]));
1239
- // Only a purely-entailed scm-sco fact ever carries a walkable justification
1240
- // (see syllogise()'s toWrite mapping + isPurelyEntailed, above) — every
1241
- // other row's justification is [] (or the fact is also independently
1242
- // taught, so it is EXCLUDED here even if it happens to carry a stale one),
1243
- // so this candidate pool is naturally, correctly scoped.
874
+ // Only a purely-entailed scm-sco fact ever carries a walkable justification.
1244
875
  const entailedScRows = scRows.filter((r) => r.justification.length && isPurelyEntailed(r.provenance));
1245
876
 
1246
877
  const removed = new Set([targetId]);
@@ -1253,17 +884,10 @@ export async function retractSubClassOf(repoDir, subject, object, { budget = 50,
1253
884
  .sort((a, b) => a.subject.localeCompare(b.subject) || a.object.localeCompare(b.object));
1254
885
  if (!candidates.length) break; // fixpoint — nothing left to (re-)check
1255
886
 
1256
- // The surviving edge set for THIS round's verify walk DRed's own
1257
- // "delete a superset, then selectively rederive" discipline (§3's own
1258
- // citation, Gupta/Mumick/Subrahmanian 1993): every candidate's OWN edge
1259
- // is excluded too, alongside every OTHER candidate under suspicion this
1260
- // SAME round, not just `removed` — otherwise a candidate would trivially
1261
- // "reach itself" through its own not-yet-deleted edge (or lean on a
1262
- // sibling candidate that is itself only standing on the same broken
1263
- // premise), understating what actually still needs re-verifying. A
1264
- // candidate that reaches its target through some OTHER, untouched edge
1265
- // (a genuinely independent derivation path this fact's single persisted
1266
- // justification never recorded) correctly survives.
887
+ // The surviving edge set for THIS round's verify walk excludes every
888
+ // candidate's own edge too, not just `removed` otherwise a candidate
889
+ // could trivially "reach itself" through its own not-yet-deleted edge, or
890
+ // lean on a sibling candidate standing on the same broken premise.
1267
891
  const candidateIds = new Set(candidates.map((c) => c.id));
1268
892
  const survivingEdges = [...edgeOf.entries()]
1269
893
  .filter(([id]) => !removed.has(id) && !candidateIds.has(id))
@@ -1297,27 +921,16 @@ export async function retractSubClassOf(repoDir, subject, object, { budget = 50,
1297
921
  }
1298
922
 
1299
923
  /**
1300
- * PROOF SEARCH (not a third rule — a bounded ROOTED chase that composes the
1301
- * two rules above for a single "does `subj` reach one of `targets`?" query,
1302
- * PLAN_INFERENCE_TESTING.md §4 stage 2's proof-chain substrate). Unlike
1303
- * `deriveSubClassClosure`/`deriveTypePropagation` which each compute a
1304
- * WHOLE-GRAPH closure and then focus/budget-filter the result (so an
1305
- * unrelated derivation that merely TOUCHES the focus term as a pivot or
1306
- * object can still fill the budget before the one the caller actually wants)
1307
- * — this walks OUTWARD from `subj` only, breadth-first, stopping the instant
1308
- * a target is reached. That makes it safe to call live, per query, even over
1309
- * a large (e.g. corpus-seeded) fact store: cost is bounded by `subj`'s own
1310
- * reachable set and `maxHops`, never by how many OTHER classes the store
1311
- * happens to know about.
924
+ * PROOF SEARCH (not a third rule — a bounded rooted chase for a single "does
925
+ * `subj` reach one of `targets`?" query). Walks OUTWARD from `subj` only,
926
+ * breadth-first, stopping the instant a target is reached — cost is bounded
927
+ * by `subj`'s own reachable set and `maxHops`, not by the whole graph, unlike
928
+ * `deriveSubClassClosure`/`deriveTypePropagation`'s whole-graph closures.
1312
929
  *
1313
- * The first hop may be EITHER a taught type edge (cax-sco: `subj rdf:type
1314
- * C`) OR a taught subClassOf edge (scm-sco: `subj C`) `subj` may be an
1315
- * individual or a class. Every hop after the first is subClassOf-only (once
1316
- * "in" a class, propagation continues up the class hierarchy). Returns the
1317
- * shortest chain as an ordered `[{ subject, predicate, object }, …]` premise
1318
- * list (each already a stored fact — the caller cites its provenance), or
1319
- * null when no chain reaches `targets` within `maxHops`. Pure, no I/O,
1320
- * deterministic given the same edge lists.
930
+ * The first hop may be a taught type edge (cax-sco) or subClassOf edge
931
+ * (scm-sco); every hop after is subClassOf-only. Returns the shortest chain
932
+ * as an ordered `[{ subject, predicate, object }, …]` premise list, or null
933
+ * when no chain reaches `targets` within `maxHops`.
1321
934
  */
1322
935
  export function findIsaChain(subj, targets, typeEdges, subClassEdges, { maxHops = 6 } = {}) {
1323
936
  const targetSet = targets instanceof Set ? targets : new Set(targets || []);
@@ -1339,8 +952,7 @@ export function findIsaChain(subj, targets, typeEdges, subClassEdges, { maxHops
1339
952
  // hop counts the LENGTH of the paths currently in `frontier` (1 at the
1340
953
  // first check). Check-then-extend, and never extend past maxHops — the
1341
954
  // frontier is checked AT every length up to and including maxHops, never
1342
- // one hop beyond it (an earlier version's separate post-loop check did
1343
- // exactly that off-by-one — fixed here, regression-tested below).
955
+ // one hop beyond it.
1344
956
  const seen = new Set([subj]);
1345
957
  for (let hop = 1; hop <= maxHops && frontier.length; hop += 1) {
1346
958
  for (const { node, path } of frontier) if (targetSet.has(node)) return path;