@polycode-projects/the-mechanical-code-talker 1.10.14 → 1.11.5
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 +121 -114
- package/ROADMAP.md +14 -4
- package/bin/tmct.mjs +167 -77
- package/data/games/crates.txt +24 -0
- package/data/games/hanoi-3.txt +30 -0
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +1515 -52
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +350 -0
- package/src/import-file.mjs +86 -0
- package/src/init.mjs +46 -1
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +635 -0
- package/src/memory/core.mjs +100 -17
- package/src/memory/shacl.mjs +20 -7
- package/src/memory-ask-browser-entry.mjs +9 -7
- package/src/memory-ask-browser.bundle.js +5648 -1177
- package/src/plan-viz.mjs +409 -0
- package/src/router/drive.mjs +122 -13
- package/src/router/guardrail.mjs +5 -0
- package/src/router/registry.mjs +55 -11
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +84 -0
- package/src/sentences.mjs +19 -0
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +66 -0
- package/src/wink-model.mjs +12 -6
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5411
- package/src/viz.mjs +0 -959
package/src/router/registry.mjs
CHANGED
|
@@ -179,10 +179,54 @@ const CAPABILITIES = Object.freeze([
|
|
|
179
179
|
}),
|
|
180
180
|
]);
|
|
181
181
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
// The live capability set: the built-in frozen array is the seed; registration
|
|
183
|
+
// rebuilds `list`/`byName` wholesale so every accessor stays a plain read.
|
|
184
|
+
const buildIndex = (caps) => Object.freeze(
|
|
185
|
+
caps.reduce((m, c) => { m[c.name] = c; return m; }, Object.create(null)),
|
|
185
186
|
);
|
|
187
|
+
let list = CAPABILITIES;
|
|
188
|
+
let byName = buildIndex(list);
|
|
189
|
+
|
|
190
|
+
function deepFreeze(value) {
|
|
191
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
192
|
+
Object.freeze(value);
|
|
193
|
+
for (const k of Object.keys(value)) deepFreeze(value[k]);
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Register a capability at runtime (e.g. a taught action family bridged in by
|
|
199
|
+
* src/router/taught.mjs). `readOnly` must be an explicit boolean; a
|
|
200
|
+
* `readOnly: false` record is forced `dispatchable: false` — the guardrail's
|
|
201
|
+
* candidate enrichment re-dispatches a tool once per tied candidate, which is
|
|
202
|
+
* only safe when dispatch performs no writes. Returns an `unregister()`
|
|
203
|
+
* disposer. */
|
|
204
|
+
export function registerCapability(cap) {
|
|
205
|
+
const name = cap && typeof cap.name === "string" ? cap.name.trim() : "";
|
|
206
|
+
if (!name) throw new Error("registerCapability: a non-empty name is required");
|
|
207
|
+
if (byName[name]) throw new Error(`registerCapability: "${name}" is already registered`);
|
|
208
|
+
if (!Array.isArray(cap.parameters) || !Array.isArray(cap.preconditions)) {
|
|
209
|
+
throw new Error(`registerCapability: "${name}" needs parameters[] and preconditions[]`);
|
|
210
|
+
}
|
|
211
|
+
if (!cap.effects || !Array.isArray(cap.effects.add) || !Array.isArray(cap.effects.del)) {
|
|
212
|
+
throw new Error(`registerCapability: "${name}" needs effects {add: [], del: []}`);
|
|
213
|
+
}
|
|
214
|
+
if (typeof cap.readOnly !== "boolean") {
|
|
215
|
+
throw new Error(`registerCapability: "${name}" needs an explicit boolean readOnly`);
|
|
216
|
+
}
|
|
217
|
+
const rec = deepFreeze({
|
|
218
|
+
type: VOCAB.Capability,
|
|
219
|
+
...cap,
|
|
220
|
+
name,
|
|
221
|
+
dispatchable: cap.readOnly === true ? cap.dispatchable !== false : false,
|
|
222
|
+
});
|
|
223
|
+
list = Object.freeze([...list, rec]);
|
|
224
|
+
byName = buildIndex(list);
|
|
225
|
+
return function unregister() {
|
|
226
|
+
list = Object.freeze(list.filter((c) => c !== rec));
|
|
227
|
+
byName = buildIndex(list);
|
|
228
|
+
};
|
|
229
|
+
}
|
|
186
230
|
|
|
187
231
|
// ---- unregistered dispatch tools ---------------------------------------------
|
|
188
232
|
// Dispatch tools not yet registered; each names the precondition work it needs first.
|
|
@@ -199,28 +243,28 @@ export const REGISTRY = Object.freeze({
|
|
|
199
243
|
vocab: VOCAB,
|
|
200
244
|
kinds: KINDS,
|
|
201
245
|
precond: PRECOND,
|
|
202
|
-
capabilities
|
|
246
|
+
get capabilities() { return list; },
|
|
203
247
|
});
|
|
204
248
|
|
|
205
249
|
// ---- pure accessors ---------------------------------------------------------
|
|
206
250
|
|
|
207
|
-
/** All declared capabilities (the operator set). */
|
|
208
|
-
export function capabilities() { return
|
|
251
|
+
/** All declared capabilities (the operator set, plus any registered at runtime). */
|
|
252
|
+
export function capabilities() { return list; }
|
|
209
253
|
|
|
210
254
|
/** The capability named `n`, or undefined. */
|
|
211
|
-
export function capabilityByName(n) { return
|
|
255
|
+
export function capabilityByName(n) { return byName[n]; }
|
|
212
256
|
|
|
213
257
|
/** True iff `n` names a declared capability. */
|
|
214
|
-
export function isCapability(n) { return Boolean(
|
|
258
|
+
export function isCapability(n) { return Boolean(byName[n]); }
|
|
215
259
|
|
|
216
260
|
/** The parameter slots of capability `n` (empty array if unknown/no-arg). */
|
|
217
|
-
export function parametersOf(n) { return
|
|
261
|
+
export function parametersOf(n) { return byName[n]?.parameters ?? []; }
|
|
218
262
|
|
|
219
263
|
/** The preconditions of capability `n` (the safety gate the guardrail checks). */
|
|
220
|
-
export function preconditionsOf(n) { return
|
|
264
|
+
export function preconditionsOf(n) { return byName[n]?.preconditions ?? []; }
|
|
221
265
|
|
|
222
266
|
/** The effects of capability `n` — `{ add, del }` (the proof-chain contribution). */
|
|
223
|
-
export function effectsOf(n) { return
|
|
267
|
+
export function effectsOf(n) { return byName[n]?.effects ?? { add: [], del: [] }; }
|
|
224
268
|
|
|
225
269
|
/** The set of arg keys capability `n` accepts (for the guardrail's unknown-arg
|
|
226
270
|
* check). Returns a Set of strings. */
|
package/src/router/resolver.mjs
CHANGED
|
@@ -94,6 +94,19 @@ export function backwardChain(topic) {
|
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** Backward-chain a WORLD goal — a state predicate like "rest-on" that a
|
|
98
|
+
* taught action's effect establishes — to the registered record whose
|
|
99
|
+
* add-list carries the matching taught:world-effect (the bridge
|
|
100
|
+
* src/router/taught.mjs registers). Pure over the registry; null when no
|
|
101
|
+
* taught record achieves the predicate. The sibling of backwardChain above,
|
|
102
|
+
* which only ever matches epistemic `knows` effects. */
|
|
103
|
+
export function backwardChainWorld(predicate) {
|
|
104
|
+
for (const cap of capabilities()) {
|
|
105
|
+
if (effectsOf(cap.name).add.some((e) => e.pred === "taught:world-effect" && e.predicate === predicate)) return cap;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
// Stopwords for the imperative-frame entity extractor. Deliberately generous: a wrong
|
|
98
111
|
// pick is caught by the resolveObject miss -> honest refuse, never emitted.
|
|
99
112
|
const STOP = new Set([
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// taught.mjs — bridge taught action-Rule families into the capability registry.
|
|
2
|
+
//
|
|
3
|
+
// A taught game action ("you can move a disk onto a peg" + its preconditions
|
|
4
|
+
// and effect) becomes a registered capability record so the router's operator
|
|
5
|
+
// model covers taught actions and built-in query tools alike. Registered
|
|
6
|
+
// records carry readOnly: false, so the guardrail never dispatches them — the
|
|
7
|
+
// resolver also never selects them on its own, because it backward-chains over
|
|
8
|
+
// `knows` add-effects and these records carry world-triple effects instead.
|
|
9
|
+
|
|
10
|
+
import { readRuleRows } from "../memory/core.mjs";
|
|
11
|
+
import { capabilityByName, registerCapability } from "./registry.mjs";
|
|
12
|
+
|
|
13
|
+
const ACTION_KINDS = new Set(["action-signature", "action-precond", "action-effect", "action-constraint"]);
|
|
14
|
+
|
|
15
|
+
/** Group a memory payload's action-family rule rows by rule name. */
|
|
16
|
+
export function actionFamilies(memory) {
|
|
17
|
+
const families = new Map();
|
|
18
|
+
for (const row of readRuleRows(memory)) {
|
|
19
|
+
if (!ACTION_KINDS.has(row.kind)) continue;
|
|
20
|
+
if (!families.has(row.name)) families.set(row.name, []);
|
|
21
|
+
families.get(row.name).push(row);
|
|
22
|
+
}
|
|
23
|
+
return families;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Map one action family to a registrable capability record. */
|
|
27
|
+
export function capabilityFromActionRules(name, family) {
|
|
28
|
+
const signatures = family.filter((r) => r.kind === "action-signature");
|
|
29
|
+
const preconds = family.filter((r) => r.kind === "action-precond");
|
|
30
|
+
const effects = family.filter((r) => r.kind === "action-effect");
|
|
31
|
+
const constraints = family.filter((r) => r.kind === "action-constraint");
|
|
32
|
+
return {
|
|
33
|
+
name: `taught:${name}`,
|
|
34
|
+
label: name,
|
|
35
|
+
question: `apply the taught action "${name}" to the world state`,
|
|
36
|
+
readOnly: false,
|
|
37
|
+
parameters: [
|
|
38
|
+
{ name: "subject", classes: [...new Set(signatures.map((s) => s.slots.subjectClass))].sort() },
|
|
39
|
+
{ name: "target", classes: [...new Set(signatures.map((s) => s.slots.targetClass))].sort() },
|
|
40
|
+
],
|
|
41
|
+
preconditions: [
|
|
42
|
+
...preconds.map((p) => ({
|
|
43
|
+
pred: "taught:world-precond",
|
|
44
|
+
shape: p.slots.shape,
|
|
45
|
+
predicate: p.slots.predicate,
|
|
46
|
+
role: p.slots.role,
|
|
47
|
+
scope: p.slots.scope,
|
|
48
|
+
})),
|
|
49
|
+
// A constraint is a precondition on the SUCCESSOR state; it rides the
|
|
50
|
+
// record's precondition list so a bridged family stays complete.
|
|
51
|
+
...constraints.map((c) => ({
|
|
52
|
+
pred: "taught:world-constraint",
|
|
53
|
+
left: c.slots.left,
|
|
54
|
+
right: c.slots.right,
|
|
55
|
+
guard: c.slots.guard,
|
|
56
|
+
})),
|
|
57
|
+
],
|
|
58
|
+
effects: {
|
|
59
|
+
add: effects.map((e) => ({
|
|
60
|
+
pred: "taught:world-effect",
|
|
61
|
+
predicate: e.slots.predicate,
|
|
62
|
+
subjectRole: e.slots.subjectRole,
|
|
63
|
+
objectRole: e.slots.objectRole,
|
|
64
|
+
})),
|
|
65
|
+
del: effects.map((e) => ({
|
|
66
|
+
pred: "taught:world-effect-replaced",
|
|
67
|
+
predicate: e.slots.predicate,
|
|
68
|
+
subjectRole: e.slots.subjectRole,
|
|
69
|
+
})),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Register every taught action family in `memory`, idempotently (a name that
|
|
75
|
+
* is already registered is skipped). Returns the new registrations'
|
|
76
|
+
* unregister disposers. */
|
|
77
|
+
export function registerTaughtActions(memory) {
|
|
78
|
+
const disposers = [];
|
|
79
|
+
for (const [name, family] of actionFamilies(memory)) {
|
|
80
|
+
if (capabilityByName(`taught:${name}`)) continue;
|
|
81
|
+
disposers.push(registerCapability(capabilityFromActionRules(name, family)));
|
|
82
|
+
}
|
|
83
|
+
return disposers;
|
|
84
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// sentences.mjs — sentence-boundary splitting, shared by the extract-facts
|
|
2
|
+
// script, the chat one-shot CLI, and runTurn's multi-sentence pre-split.
|
|
3
|
+
|
|
4
|
+
import { winkInstance } from "./wink-model.mjs";
|
|
5
|
+
|
|
6
|
+
/** Split text into trimmed, non-empty sentences via wink-nlp's own
|
|
7
|
+
* sentence-boundary detection — never a naive regex split, matching the
|
|
8
|
+
* ONE way every other adapter in this repo reaches wink (wink-model.mjs).
|
|
9
|
+
* Returns [] (never throws) when wink isn't available or the text is
|
|
10
|
+
* blank — the same honest-degrade idiom every wink-model.mjs consumer
|
|
11
|
+
* already uses. */
|
|
12
|
+
export function splitSentences(text) {
|
|
13
|
+
const nlp = winkInstance();
|
|
14
|
+
if (!nlp) return [];
|
|
15
|
+
const raw = String(text ?? "");
|
|
16
|
+
if (!raw.trim()) return [];
|
|
17
|
+
const doc = nlp.readDoc(raw);
|
|
18
|
+
return doc.sentences().out().map((s) => s.trim()).filter(Boolean);
|
|
19
|
+
}
|
package/src/syllogise.mjs
CHANGED
|
@@ -694,6 +694,7 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
694
694
|
const trustByTriple = new Map();
|
|
695
695
|
for (const r of rows) trustByTriple.set(`${r.subject}${SEP}${r.predicate}${SEP}${r.object}`, r.trust);
|
|
696
696
|
const premiseTrust = (s, p, o) => trustByTriple.get(`${s}${SEP}${p}${SEP}${o}`);
|
|
697
|
+
const hasTriple = (s, p, o) => trustByTriple.has(`${s}${SEP}${p}${SEP}${o}`);
|
|
697
698
|
const numericOnly = (arr) => arr.filter((t) => typeof t === "number");
|
|
698
699
|
|
|
699
700
|
const scmDerived = deriveSubClassClosure(subClassEdges, { depth, budget, focus: normalizedFocus });
|
|
@@ -732,11 +733,12 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
732
733
|
...scmDerived.map((d) => ({
|
|
733
734
|
subject: d.subject, predicate: SUBCLASS_PREDICATE, object: d.object,
|
|
734
735
|
provenance: ENTAILED_PROVENANCE,
|
|
735
|
-
// Persisted justification
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
736
|
+
// Persisted justification: the premise fact ids this conclusion rode
|
|
737
|
+
// (a⊑b, b⊑c) — content-addressed ids work even when a premise is
|
|
738
|
+
// itself an entailment this same pass just derived. Read back by
|
|
739
|
+
// retractSubClassOf (below) to find every entailment a retracted
|
|
740
|
+
// premise could have supported. All five rules persist one, each
|
|
741
|
+
// citing its own premise shape.
|
|
740
742
|
justification: [
|
|
741
743
|
factIdForTriple(d.subject, SUBCLASS_PREDICATE, d.via),
|
|
742
744
|
factIdForTriple(d.via, SUBCLASS_PREDICATE, d.object),
|
|
@@ -745,15 +747,24 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
745
747
|
...caxDerived.map((d) => ({
|
|
746
748
|
subject: d.subject, predicate: TYPE_PREDICATE, object: d.object,
|
|
747
749
|
provenance: ENTAILED_TYPE_PROVENANCE,
|
|
750
|
+
// The ⊑ premise is cited as the DIRECT via⊑object edge even when the
|
|
751
|
+
// taught chain is multi-hop: scm-sco materializes that edge (this same
|
|
752
|
+
// pass or an earlier one), and retraction re-VERIFIES every candidate
|
|
753
|
+
// anyway, so a citation left dangling by budget truncation is inert.
|
|
754
|
+
justification: [
|
|
755
|
+
factIdForTriple(d.subject, TYPE_PREDICATE, d.via),
|
|
756
|
+
factIdForTriple(d.via, SUBCLASS_PREDICATE, d.object),
|
|
757
|
+
],
|
|
748
758
|
})),
|
|
749
759
|
...dwDerived.map((d) => {
|
|
750
760
|
// disjointWith is symmetric, taught as ONE direction — the premise row
|
|
751
|
-
// could be stored either (viaClass, disjointWith, object) or its
|
|
752
|
-
|
|
753
|
-
|
|
761
|
+
// could be stored either (viaClass, disjointWith, object) or its
|
|
762
|
+
// mirror; resolve which, so the justification cites a real stored id.
|
|
763
|
+
const dwStoredForward = hasTriple(d.viaClass, DISJOINT_PREDICATE, d.object);
|
|
764
|
+
const [dwS, dwO] = dwStoredForward ? [d.viaClass, d.object] : [d.object, d.viaClass];
|
|
754
765
|
const premiseTrusts = numericOnly([
|
|
755
766
|
premiseTrust(d.subject, TYPE_PREDICATE, d.viaType),
|
|
756
|
-
|
|
767
|
+
premiseTrust(dwS, DISJOINT_PREDICATE, dwO),
|
|
757
768
|
// the ⊑-lift premise only exists when this IS a lift (viaClass !==
|
|
758
769
|
// viaType) — a direct hit has no extra subClassOf premise to price in.
|
|
759
770
|
...(d.viaClass !== d.viaType ? [premiseTrust(d.viaType, SUBCLASS_PREDICATE, d.viaClass)] : []),
|
|
@@ -761,6 +772,11 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
761
772
|
return {
|
|
762
773
|
subject: d.subject, predicate: DISJOINT_PREDICATE, object: d.object,
|
|
763
774
|
provenance: ENTAILED_DISJOINT_PROVENANCE,
|
|
775
|
+
justification: [
|
|
776
|
+
factIdForTriple(d.subject, TYPE_PREDICATE, d.viaType),
|
|
777
|
+
factIdForTriple(dwS, DISJOINT_PREDICATE, dwO),
|
|
778
|
+
...(d.viaClass !== d.viaType ? [factIdForTriple(d.viaType, SUBCLASS_PREDICATE, d.viaClass)] : []),
|
|
779
|
+
],
|
|
764
780
|
...(premiseTrusts.length ? { premiseTrusts, ruleConfidence: CAX_DW_RULE_CONFIDENCE } : {}),
|
|
765
781
|
};
|
|
766
782
|
}),
|
|
@@ -777,6 +793,13 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
777
793
|
return {
|
|
778
794
|
subject: d.subject, predicate: TYPE_PREDICATE, object: d.object,
|
|
779
795
|
provenance: ENTAILED_SVF1_PROVENANCE,
|
|
796
|
+
justification: [
|
|
797
|
+
factIdForTriple(d.subject, d.viaProperty, d.viaValue),
|
|
798
|
+
factIdForTriple(d.viaValue, TYPE_PREDICATE, d.viaType),
|
|
799
|
+
factIdForTriple(d.object, ON_PROPERTY_PREDICATE, d.viaPropertyKey),
|
|
800
|
+
factIdForTriple(d.object, SOME_VALUES_FROM_PREDICATE, d.viaTarget),
|
|
801
|
+
...(d.viaType !== d.viaTarget ? [factIdForTriple(d.viaType, SUBCLASS_PREDICATE, d.viaTarget)] : []),
|
|
802
|
+
],
|
|
780
803
|
// same sub-1 discount as cax-dw, same reason (see CAX_DW_RULE_CONFIDENCE).
|
|
781
804
|
...(premiseTrusts.length ? { premiseTrusts, ruleConfidence: CLS_SVF1_RULE_CONFIDENCE } : {}),
|
|
782
805
|
};
|
|
@@ -794,6 +817,13 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
794
817
|
return {
|
|
795
818
|
subject: d.subject, predicate: SUBCLASS_PREDICATE, object: d.object,
|
|
796
819
|
provenance: ENTAILED_SCM_SVF_PROVENANCE,
|
|
820
|
+
justification: [
|
|
821
|
+
...(r1 ? [factIdForTriple(d.subject, ON_PROPERTY_PREDICATE, r1.property)] : []),
|
|
822
|
+
factIdForTriple(d.subject, SOME_VALUES_FROM_PREDICATE, d.viaY1),
|
|
823
|
+
...(r2 ? [factIdForTriple(d.object, ON_PROPERTY_PREDICATE, r2.property)] : []),
|
|
824
|
+
factIdForTriple(d.object, SOME_VALUES_FROM_PREDICATE, d.viaY2),
|
|
825
|
+
factIdForTriple(d.viaY1, SUBCLASS_PREDICATE, d.viaY2),
|
|
826
|
+
],
|
|
797
827
|
// same sub-1 discount as cax-dw/cls-svf1, same reason (see CAX_DW_RULE_CONFIDENCE).
|
|
798
828
|
...(premiseTrusts.length ? { premiseTrusts, ruleConfidence: SCM_SVF_RULE_CONFIDENCE } : {}),
|
|
799
829
|
};
|
|
@@ -838,21 +868,112 @@ function isPurelyEntailed(provenance) {
|
|
|
838
868
|
return tags.length > 0 && tags.every((t) => t.startsWith("entailed:"));
|
|
839
869
|
}
|
|
840
870
|
|
|
871
|
+
/** Builds the per-round VERIFY oracle for retraction: given ONLY the
|
|
872
|
+
* surviving fact rows, returns `stillDerivable(row)` — true when the row's
|
|
873
|
+
* (s,p,o) conclusion is re-derivable from survivors by the rule family that
|
|
874
|
+
* owns its predicate (scm-sco/scm-svf1 for subClassOf, cax-sco/cls-svf1 for
|
|
875
|
+
* rdf:type, cax-dw for disjointWith). One shared ancestor closure plus small
|
|
876
|
+
* indexes per round, joining exactly what each derive kernel joins. Pure,
|
|
877
|
+
* no I/O. */
|
|
878
|
+
function buildSurvivorDerivabilityCheck(rows) {
|
|
879
|
+
const subClassEdges = [];
|
|
880
|
+
const typesOf = new Map(); // x -> Set(surviving direct type classes)
|
|
881
|
+
const disjointOf = new Map(); // term -> Set(disjoint partners), symmetric
|
|
882
|
+
const onPropertyOf = new Map(); // restriction -> owl:onProperty's object
|
|
883
|
+
const someValuesFromOf = new Map(); // restriction -> owl:someValuesFrom's object
|
|
884
|
+
const propertyEdgesOf = new Map(); // x -> [[normalized predicate, y], …]
|
|
885
|
+
for (const r of rows) {
|
|
886
|
+
const pLower = String(r.predicate || "").trim().toLowerCase();
|
|
887
|
+
if (isSubClassOf(r.predicate)) subClassEdges.push([r.subject, r.object]);
|
|
888
|
+
else if (isType(r.predicate)) {
|
|
889
|
+
if (!typesOf.has(r.subject)) typesOf.set(r.subject, new Set());
|
|
890
|
+
typesOf.get(r.subject).add(r.object);
|
|
891
|
+
} else if (isDisjoint(r.predicate)) {
|
|
892
|
+
if (!disjointOf.has(r.subject)) disjointOf.set(r.subject, new Set());
|
|
893
|
+
disjointOf.get(r.subject).add(r.object);
|
|
894
|
+
if (!disjointOf.has(r.object)) disjointOf.set(r.object, new Set());
|
|
895
|
+
disjointOf.get(r.object).add(r.subject);
|
|
896
|
+
} else if (isOnProperty(r.predicate)) onPropertyOf.set(r.subject, r.object);
|
|
897
|
+
else if (isSomeValuesFrom(r.predicate)) someValuesFromOf.set(r.subject, r.object);
|
|
898
|
+
else if (!RESERVED_PREDICATES.has(pLower)) {
|
|
899
|
+
if (!propertyEdgesOf.has(r.subject)) propertyEdgesOf.set(r.subject, []);
|
|
900
|
+
propertyEdgesOf.get(r.subject).push([normFactTerm(r.predicate), r.object]);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const ancestorsOf = buildAncestorCloser(subClassEdges);
|
|
904
|
+
const reaches = (a, b) => a !== b && ancestorsOf(a).has(b);
|
|
905
|
+
const restrictionOf = (node) => {
|
|
906
|
+
const property = onPropertyOf.get(node);
|
|
907
|
+
const target = someValuesFromOf.get(node);
|
|
908
|
+
return property && target ? { property: normFactTerm(property), target } : null;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
return (row) => {
|
|
912
|
+
if (isSubClassOf(row.predicate)) {
|
|
913
|
+
// scm-sco: some surviving ⊑-path still connects subject to object.
|
|
914
|
+
if (reaches(row.subject, row.object)) return true;
|
|
915
|
+
// scm-svf1: both ends still declared restrictions over the SAME
|
|
916
|
+
// property, with strictly ⊑-related fillers (kernel-faithful).
|
|
917
|
+
const r1 = restrictionOf(row.subject);
|
|
918
|
+
const r2 = restrictionOf(row.object);
|
|
919
|
+
return Boolean(r1 && r2 && r1.property === r2.property && reaches(r1.target, r2.target));
|
|
920
|
+
}
|
|
921
|
+
if (isType(row.predicate)) {
|
|
922
|
+
// cax-sco: a surviving direct type whose ⊑-closure reaches the class.
|
|
923
|
+
for (const c of typesOf.get(row.subject) || []) {
|
|
924
|
+
if (reaches(c, row.object)) return true;
|
|
925
|
+
}
|
|
926
|
+
// cls-svf1: the class is a still-declared restriction node — a
|
|
927
|
+
// surviving property edge whose value's type (⊑-lifted) satisfies it.
|
|
928
|
+
const rec = restrictionOf(row.object);
|
|
929
|
+
if (rec) {
|
|
930
|
+
for (const [pKey, y] of propertyEdgesOf.get(row.subject) || []) {
|
|
931
|
+
if (pKey !== rec.property) continue;
|
|
932
|
+
for (const c of typesOf.get(y) || []) {
|
|
933
|
+
if (c === rec.target || reaches(c, rec.target)) return true;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
if (isDisjoint(row.predicate)) {
|
|
940
|
+
// cax-dw: a surviving type whose ⊑-closure meets a surviving
|
|
941
|
+
// disjointWith partner equal to the conclusion's object.
|
|
942
|
+
for (const c of typesOf.get(row.subject) || []) {
|
|
943
|
+
for (const d of [c, ...ancestorsOf(c)]) {
|
|
944
|
+
if (disjointOf.get(d)?.has(row.object)) return true;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
// An entailed predicate no rule family here owns: nothing can re-check
|
|
950
|
+
// it, so it is never removed on a stale citation alone.
|
|
951
|
+
return true;
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
841
955
|
/**
|
|
842
|
-
* A scoped retraction slice: JTMS-style dependency-directed removal
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* removed id
|
|
846
|
-
*
|
|
956
|
+
* A scoped retraction slice: JTMS-style dependency-directed removal.
|
|
957
|
+
* Retracting `subject ⊑ object` removes the fact, then cascades to any
|
|
958
|
+
* purely-entailed fact — across all five rules' conclusions — whose persisted
|
|
959
|
+
* justification cites a removed id. Each candidate is VERIFIED (re-derivable
|
|
960
|
+
* from the surviving facts, not just "cited a removed id") before it is
|
|
847
961
|
* actually removed, since a fact can have a second, independent derivation
|
|
848
962
|
* path (a⊑b⊑d AND a⊑c⊑d both license a⊑d) that a bare delete-by-justification
|
|
849
963
|
* walk would wrongly discard. Repeats in rounds — a removed mid-chain link
|
|
850
964
|
* can ripple — bounded by `budget` (max facts examined+removed) and `depth`
|
|
851
965
|
* (max cascade rounds).
|
|
852
966
|
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
967
|
+
* The entry point stays subClassOf-rooted because chat's recognized
|
|
968
|
+
* retraction phrasings ("X is not a Y", "forget that X is a kind of Y",
|
|
969
|
+
* chat.mjs's teach lane) retract subClassOf facts; the cascade itself follows
|
|
970
|
+
* justifications into every rule's conclusions (transitive ⊑, propagated
|
|
971
|
+
* types, disjointness violations, restriction membership and subsumption).
|
|
972
|
+
*
|
|
973
|
+
* A survivor keeps its stale, still-single justification as-is; a later
|
|
974
|
+
* retraction of its OTHER supporting path therefore won't re-examine it.
|
|
975
|
+
* Re-grounding survivors — or tracking every alternate justification set —
|
|
976
|
+
* is the ATMS horizon (PLAN_SYLLOGIST.md §3), not this bounded slice.
|
|
856
977
|
*
|
|
857
978
|
* Returns { retracted, count, budget, depth, truncated, found } — `found` is
|
|
858
979
|
* false when `subject ⊑ object` was never a stored fact.
|
|
@@ -866,43 +987,38 @@ export async function retractSubClassOf(repoDir, subject, object, { budget = 50,
|
|
|
866
987
|
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
867
988
|
if (!byId.has(targetId)) return { retracted: [], count: 0, budget, depth, truncated: false, found: false };
|
|
868
989
|
|
|
869
|
-
//
|
|
870
|
-
//
|
|
871
|
-
|
|
872
|
-
const scRows = rows.filter((r) => isSubClassOf(r.predicate));
|
|
873
|
-
const edgeOf = new Map(scRows.map((r) => [r.id, [r.subject, r.object]]));
|
|
874
|
-
// Only a purely-entailed scm-sco fact ever carries a walkable justification.
|
|
875
|
-
const entailedScRows = scRows.filter((r) => r.justification.length && isPurelyEntailed(r.provenance));
|
|
990
|
+
// Only a purely-entailed fact ever carries a walkable justification —
|
|
991
|
+
// a fact later independently taught is never a cascade candidate at all.
|
|
992
|
+
const entailedRows = rows.filter((r) => r.justification.length && isPurelyEntailed(r.provenance));
|
|
876
993
|
|
|
877
994
|
const removed = new Set([targetId]);
|
|
878
995
|
const order = [targetId]; // deterministic report order: target first, then removal order
|
|
879
996
|
let truncated = false;
|
|
880
997
|
let round = 0;
|
|
881
998
|
for (; round < depth; round += 1) {
|
|
882
|
-
const candidates =
|
|
999
|
+
const candidates = entailedRows
|
|
883
1000
|
.filter((r) => !removed.has(r.id) && r.justification.some((j) => removed.has(j)))
|
|
884
|
-
.sort((a, b) => a.subject.localeCompare(b.subject) || a.object.localeCompare(b.object));
|
|
1001
|
+
.sort((a, b) => a.subject.localeCompare(b.subject) || a.predicate.localeCompare(b.predicate) || a.object.localeCompare(b.object));
|
|
885
1002
|
if (!candidates.length) break; // fixpoint — nothing left to (re-)check
|
|
886
1003
|
|
|
887
|
-
// The surviving
|
|
888
|
-
// candidate's own
|
|
1004
|
+
// The surviving fact set for THIS round's verify walk excludes every
|
|
1005
|
+
// candidate's own row too, not just `removed` — otherwise a candidate
|
|
889
1006
|
// could trivially "reach itself" through its own not-yet-deleted edge, or
|
|
890
1007
|
// lean on a sibling candidate standing on the same broken premise.
|
|
891
1008
|
const candidateIds = new Set(candidates.map((c) => c.id));
|
|
892
|
-
const
|
|
893
|
-
.filter((
|
|
894
|
-
|
|
895
|
-
const ancestorsOf = buildAncestorCloser(survivingEdges);
|
|
1009
|
+
const stillDerivable = buildSurvivorDerivabilityCheck(
|
|
1010
|
+
rows.filter((r) => !removed.has(r.id) && !candidateIds.has(r.id)),
|
|
1011
|
+
);
|
|
896
1012
|
|
|
897
1013
|
let progressed = false;
|
|
898
1014
|
let hitBudget = false;
|
|
899
1015
|
for (const c of candidates) {
|
|
900
1016
|
if (removed.size >= budget) { hitBudget = true; break; }
|
|
901
|
-
// does
|
|
902
|
-
// surviving
|
|
903
|
-
// through)? A survivor keeps its (now possibly re-groundable,
|
|
904
|
-
// TRUE) fact and is never re-examined again this call.
|
|
905
|
-
if (
|
|
1017
|
+
// does the conclusion still hold WITHOUT the retracted premise, via ANY
|
|
1018
|
+
// surviving derivation (not just the one this fact was originally
|
|
1019
|
+
// derived through)? A survivor keeps its (now possibly re-groundable,
|
|
1020
|
+
// still TRUE) fact and is never re-examined again this call.
|
|
1021
|
+
if (stillDerivable(c)) continue; // a second, independent derivation still supports it — keep
|
|
906
1022
|
removed.add(c.id);
|
|
907
1023
|
order.push(c.id);
|
|
908
1024
|
progressed = true;
|
|
@@ -913,7 +1029,7 @@ export async function retractSubClassOf(repoDir, subject, object, { budget = 50,
|
|
|
913
1029
|
if (!truncated && round >= depth) {
|
|
914
1030
|
// depth exhausted, not a natural fixpoint — honestly flag it if a
|
|
915
1031
|
// pending candidate would still have been checked next round.
|
|
916
|
-
truncated =
|
|
1032
|
+
truncated = entailedRows.some((r) => !removed.has(r.id) && r.justification.some((j) => removed.has(j)));
|
|
917
1033
|
}
|
|
918
1034
|
|
|
919
1035
|
const { removed: actuallyRemoved } = await removeFacts(repoDir, order);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// viz-theme.mjs — the shared assets for tmct's generated HTML pages
|
|
2
|
+
// (the ledger explorer and the plan player): the visual token table
|
|
3
|
+
// (PLAN_VIZ_LEDGER.md's reference values) plus the escaping helpers every
|
|
4
|
+
// page builder needs.
|
|
5
|
+
//
|
|
6
|
+
// Trust tiers are precomputed rgba() values per provenance color so pages
|
|
7
|
+
// render identically on browsers without color-mix() support.
|
|
8
|
+
|
|
9
|
+
export const SERIF_STACK = `"Charter", "Bitstream Charter", Georgia, "Times New Roman", serif`;
|
|
10
|
+
export const MONO_STACK = `ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace`;
|
|
11
|
+
|
|
12
|
+
/** Escape untrusted text for safe placement inside HTML content/attributes. */
|
|
13
|
+
export function escapeHtml(s) {
|
|
14
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** JSON-embed page data into a `<script>` tag safely — escape `</` so a
|
|
18
|
+
* label/id containing "</script>" can't break out of the tag, and escape
|
|
19
|
+
* U+2028/U+2029 (valid in JSON strings, invalid unescaped in JS source). */
|
|
20
|
+
export function embedJson(value) {
|
|
21
|
+
return JSON.stringify(value)
|
|
22
|
+
.replace(/</g, "\\u003c")
|
|
23
|
+
.replace(/\u2028/g, "\\u2028")
|
|
24
|
+
.replace(/\u2029/g, "\\u2029");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** hex "#RRGGBB" -> "rgba(r, g, b, a)" */
|
|
28
|
+
function rgba(hex, alpha) {
|
|
29
|
+
const n = parseInt(hex.slice(1), 16);
|
|
30
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const TOKENS = Object.freeze({
|
|
34
|
+
light: Object.freeze({
|
|
35
|
+
bg: "#F7F6F2", ink: "#23272B", muted: "#6E7168", line: "#DDD9D0", card: "#FFFFFF",
|
|
36
|
+
taught: "#2E7D4F", corpus: "#5A80AC", entail: "#B07C2E", alert: "#B0503F",
|
|
37
|
+
}),
|
|
38
|
+
dark: Object.freeze({
|
|
39
|
+
bg: "#15181C", ink: "#E7E5DF", muted: "#9A9E95", line: "#2B3036", card: "#1C2126",
|
|
40
|
+
taught: "#5FBE8B", corpus: "#6C93BF", entail: "#D9A554", alert: "#D08070",
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const TIER_ALPHA = [0.35, 0.65, 1.0]; // trust tiers 1..3
|
|
45
|
+
|
|
46
|
+
function tokenBlock(t) {
|
|
47
|
+
const tiers = (name) =>
|
|
48
|
+
TIER_ALPHA.map((a, i) => `--${name}-t${i + 1}: ${rgba(t[name], a)};`).join(" ");
|
|
49
|
+
return [
|
|
50
|
+
`--bg: ${t.bg}; --ink: ${t.ink}; --muted: ${t.muted}; --line: ${t.line}; --card: ${t.card};`,
|
|
51
|
+
`--taught: ${t.taught}; --corpus: ${t.corpus}; --entail: ${t.entail}; --alert: ${t.alert};`,
|
|
52
|
+
tiers("taught"), tiers("corpus"), tiers("entail"),
|
|
53
|
+
`--taught-soft: ${rgba(t.taught, 0.12)}; --corpus-soft: ${rgba(t.corpus, 0.12)};`,
|
|
54
|
+
`--entail-soft: ${rgba(t.entail, 0.14)}; --alert-soft: ${rgba(t.alert, 0.12)};`,
|
|
55
|
+
].join(" ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The token table as CSS custom properties: light by default, dark via the
|
|
59
|
+
* OS preference, and explicit data-theme overrides winning in both
|
|
60
|
+
* directions (the viewer's toggle stamps data-theme on the root). */
|
|
61
|
+
export const THEME_TOKENS_CSS = `
|
|
62
|
+
:root { color-scheme: light dark; ${tokenBlock(TOKENS.light)} }
|
|
63
|
+
@media (prefers-color-scheme: dark) { :root { ${tokenBlock(TOKENS.dark)} } }
|
|
64
|
+
:root[data-theme="dark"] { ${tokenBlock(TOKENS.dark)} }
|
|
65
|
+
:root[data-theme="light"] { ${tokenBlock(TOKENS.light)} }
|
|
66
|
+
`;
|
package/src/wink-model.mjs
CHANGED
|
@@ -21,6 +21,7 @@ let cached; // undefined = not tried yet; null = unavailable (tried once, honest
|
|
|
21
21
|
export function registerWinkModel(factory) {
|
|
22
22
|
injected = factory;
|
|
23
23
|
cached = undefined;
|
|
24
|
+
instance = undefined;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/** Load `{ winkNLP, model }` once, or null when wink isn't available. Prefers a
|
|
@@ -46,15 +47,20 @@ function nodeRequireWink() {
|
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
let instance; // undefined = not built yet; null = construction failed once
|
|
51
|
+
|
|
52
|
+
/** Convenience: the constructed `nlp` instance (`winkNLP(model)`) or null.
|
|
53
|
+
* Cached: repeated `winkNLP(model)` construction accumulates module-level
|
|
54
|
+
* state inside the model package until V8 throws "Invalid string length",
|
|
55
|
+
* after which every later construction fails for the life of the process. */
|
|
52
56
|
export function winkInstance() {
|
|
57
|
+
if (instance !== undefined) return instance;
|
|
53
58
|
const loaded = loadWinkModel();
|
|
54
|
-
if (!loaded) return null;
|
|
59
|
+
if (!loaded) { instance = null; return null; }
|
|
55
60
|
try {
|
|
56
|
-
|
|
61
|
+
instance = loaded.winkNLP(loaded.model);
|
|
57
62
|
} catch {
|
|
58
|
-
|
|
63
|
+
instance = null;
|
|
59
64
|
}
|
|
65
|
+
return instance;
|
|
60
66
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// ask-browser-entry.mjs — the esbuild entry for `tmct viz`'s embedded "Ask the
|
|
2
|
-
// graph" chat panel: a real NL chat running client-side against the embedded
|
|
3
|
-
// graph, via tmct's own JS engine (esbuild + a Node-builtin stub plugin
|
|
4
|
-
// bundles tmct's real ask() into a single browser IIFE).
|
|
5
|
-
//
|
|
6
|
-
import { ask, parseQuery } from "./ask.mjs";
|
|
7
|
-
import {
|
|
8
|
-
parseEntities, spiralExpand, mostRecentIndividual, derivedUpdatedAt, MEMORY_SPIRAL_EXPAND_KINDS,
|
|
9
|
-
MEMORY_FACT_LINK_KINDS, buildVizNodesAndEdges, deriveFactTermGraph, pickLegendDimension, legendValueFor,
|
|
10
|
-
edgeKindsFor, collapseToTopN,
|
|
11
|
-
} from "./codegraph.mjs";
|
|
12
|
-
|
|
13
|
-
// Exported so the viewer page's client-side recentre/edge-kind-toggle/
|
|
14
|
-
// dimension-switcher reuses the same computation as the CLI, not a second copy.
|
|
15
|
-
globalThis.tmctViz = {
|
|
16
|
-
ask, parseQuery, parseEntities, spiralExpand, mostRecentIndividual, derivedUpdatedAt,
|
|
17
|
-
MEMORY_SPIRAL_EXPAND_KINDS, MEMORY_FACT_LINK_KINDS, buildVizNodesAndEdges,
|
|
18
|
-
deriveFactTermGraph, pickLegendDimension, legendValueFor, edgeKindsFor, collapseToTopN,
|
|
19
|
-
};
|