@polycode-projects/the-mechanical-code-talker 1.9.2 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +441 -202
- package/bin/tmct.mjs +126 -1
- package/package.json +4 -2
- package/src/answer-variants.mjs +8 -36
- package/src/ask-browser-entry.mjs +5 -23
- package/src/ask-browser.bundle.js +1 -2
- package/src/ask-nlp.mjs +9 -23
- package/src/ask-vocab.mjs +139 -589
- package/src/ask.mjs +627 -1729
- package/src/chat.mjs +1684 -2872
- package/src/cli-args.mjs +14 -28
- package/src/codegraph.mjs +236 -644
- package/src/completions/complete.mjs +18 -62
- package/src/completions/graph-adapter.mjs +14 -60
- package/src/completions/group.mjs +12 -68
- package/src/completions/infer.mjs +38 -126
- package/src/completions/prune.mjs +17 -70
- package/src/completions/rank.mjs +16 -69
- package/src/completions/search.mjs +8 -31
- package/src/concept.mjs +32 -88
- package/src/conformance.mjs +11 -15
- package/src/corpus/conceptnet.mjs +31 -89
- package/src/corpus/templates.mjs +19 -45
- package/src/corpus/unknown-ingest.mjs +31 -92
- package/src/embed.mjs +10 -22
- package/src/extensions.mjs +50 -154
- package/src/finish.mjs +35 -91
- package/src/grammar/ace.mjs +16 -40
- package/src/grammar/assert.mjs +1 -1
- package/src/grammar/lexicon-core.json +1 -1
- package/src/grammar/lexicon.mjs +9 -27
- package/src/graph-merge.mjs +2 -3
- package/src/hash.mjs +6 -14
- package/src/index.mjs +6 -10
- package/src/init.mjs +38 -125
- package/src/interpret/fuzzy.mjs +10 -29
- package/src/interpret/merge.mjs +9 -27
- package/src/interpret/normalize.mjs +137 -585
- package/src/interpret/pipeline.mjs +23 -71
- package/src/interpret/strategies/ace.mjs +7 -31
- package/src/interpret/strategies/constructions.mjs +14 -41
- package/src/interpret/strategies/grammar.mjs +21 -60
- package/src/interpret/strategies/keywords.mjs +42 -131
- package/src/interpret/strategies/noise-strip.mjs +18 -89
- package/src/memory/bias.mjs +11 -54
- package/src/memory/blocks.mjs +18 -69
- package/src/memory/core.mjs +171 -591
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +7 -25
- package/src/memory/shacl.mjs +10 -39
- package/src/memory/trust.mjs +26 -127
- package/src/memory-ask-browser-entry.mjs +7 -30
- package/src/memory-ask-browser.bundle.js +1 -1
- package/src/paraphrase.mjs +20 -53
- package/src/planning.mjs +15 -157
- package/src/prose-nlp.mjs +4 -17
- package/src/prose.mjs +19 -67
- package/src/providers/bootstrap.mjs +1 -2
- package/src/providers/fixture.mjs +1 -2
- package/src/providers/graph-service.mjs +28 -59
- package/src/repository-interface.mjs +6 -8
- package/src/router/drive.mjs +183 -0
- package/src/router/goal-reasoner.mjs +66 -231
- package/src/router/guardrail.mjs +20 -58
- package/src/router/planner.mjs +15 -46
- package/src/router/registry.mjs +13 -43
- package/src/router/resolver.mjs +46 -131
- package/src/router/results.mjs +231 -0
- package/src/schema-docs.mjs +10 -27
- package/src/server-http.mjs +10 -19
- package/src/server.mjs +22 -28
- package/src/sessions.mjs +15 -30
- package/src/source-slice.mjs +5 -7
- package/src/source.mjs +10 -20
- package/src/syllogise.mjs +187 -575
- package/src/telemetry.mjs +3 -3
- package/src/toml-config.mjs +4 -4
- package/src/tui/app.mjs +9 -19
- package/src/viz.mjs +66 -123
- package/src/wink-model.mjs +10 -24
package/src/syllogise.mjs
CHANGED
|
@@ -1,105 +1,28 @@
|
|
|
1
|
-
// syllogise.mjs — tmct's speculative-inference engine
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
15
|
-
// - scm-sco:
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// -
|
|
19
|
-
//
|
|
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
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
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
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
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
|
|
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
|
|
119
|
-
* provable "no", never a guessed one
|
|
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
|
-
/**
|
|
130
|
-
*
|
|
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
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
179
|
-
*
|
|
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
|
|
278
|
-
*
|
|
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). `
|
|
306
|
-
* `
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
* `
|
|
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"
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
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
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
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),
|
|
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
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
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. `
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
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"
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
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
|
|
560
|
-
* `
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
* `
|
|
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
|
-
/**
|
|
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
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
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
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
* `
|
|
690
|
-
*
|
|
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 (
|
|
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
|
-
/**
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
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
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
786
|
-
*
|
|
787
|
-
* `subject` has a `onClass`" —
|
|
788
|
-
* `cls-maxc1`'s per-individual ABox contradiction
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
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
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
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
|
-
*
|
|
826
|
-
*
|
|
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
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
* (
|
|
900
|
-
*
|
|
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
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
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 }
|
|
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
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
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
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
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
|
|
990
|
-
//
|
|
991
|
-
//
|
|
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
|
|
998
|
-
//
|
|
999
|
-
//
|
|
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
|
|
1009
|
-
// two restrictions
|
|
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
|
|
1024
|
-
//
|
|
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
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
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 (
|
|
1159
|
-
*
|
|
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
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
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
|
-
*
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
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 } — `
|
|
1219
|
-
*
|
|
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
|
|
1257
|
-
//
|
|
1258
|
-
//
|
|
1259
|
-
//
|
|
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
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
* `
|
|
1304
|
-
*
|
|
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
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
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
|
|
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;
|