@polycode-projects/the-mechanical-code-talker 1.8.12 → 1.8.15
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 +35 -11
- package/ROADMAP.md +7 -136
- package/data/templates/grammar-rules.toml +1 -1
- package/package.json +1 -1
- package/src/ask-browser-entry.mjs +0 -8
- package/src/ask-browser.bundle.js +68 -4
- package/src/ask-nlp.mjs +0 -9
- package/src/ask.mjs +155 -24
- package/src/chat.mjs +200 -8
- package/src/codegraph.mjs +75 -0
- package/src/interpret/normalize.mjs +45 -7
- package/src/memory/core.mjs +63 -0
- package/src/paraphrase.mjs +131 -0
- package/src/router/planner.mjs +0 -4
- package/src/server.mjs +8 -1
- package/src/source-slice.mjs +1 -2
- package/src/syllogise.mjs +160 -1
|
@@ -152,6 +152,50 @@ export function correctMisspellings(text) {
|
|
|
152
152
|
return String(text || "").replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/** FILLER_WORDS (ask-vocab.mjs), pre-built into one alternation once at module
|
|
156
|
+
* load — same "build the regex from the table once, reuse it" discipline as
|
|
157
|
+
* every other closed-vocabulary regex in this file (CONTRACTION_RE,
|
|
158
|
+
* MISSPELLING_RE, …), rather than rebuilding it inside stripFillerWords on
|
|
159
|
+
* every call. */
|
|
160
|
+
const FILLER_RE = FILLER_WORDS.length
|
|
161
|
+
? new RegExp(
|
|
162
|
+
"\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b\\s*,?",
|
|
163
|
+
"gi",
|
|
164
|
+
)
|
|
165
|
+
: null;
|
|
166
|
+
|
|
167
|
+
/** Just the filler/politeness-word strip (ask-vocab.mjs's FILLER_WORDS),
|
|
168
|
+
* standalone — the same "one piece of the pipeline, not the whole thing"
|
|
169
|
+
* need correctMisspellings above exists for: a caller with its own
|
|
170
|
+
* closed anchor regex (chat.mjs's moduleOrientLane matches "^what does …
|
|
171
|
+
* do$" itself) wants leading/embedded filler cleared WITHOUT running
|
|
172
|
+
* normalizeQuery's other, more invasive rewrites (contraction expansion,
|
|
173
|
+
* preamble/subordination/conditional frame rewrites) that can restructure
|
|
174
|
+
* the sentence in ways its own shape-matcher never expects — the exact
|
|
175
|
+
* risk correctMisspellings' own docblock describes for the same reason.
|
|
176
|
+
*
|
|
177
|
+
* A comma trailing the filler word/phrase itself (across any whitespace,
|
|
178
|
+
* e.g. "um, like," or "quickly,") is swallowed WITH it — leading
|
|
179
|
+
* conversational filler is routinely comma-spliced onto the real question
|
|
180
|
+
* ("so um, like, what does X do exactly?"), and stripping only the word
|
|
181
|
+
* leaves the comma stranded as parse-corrupting punctuation debris (a
|
|
182
|
+
* leading "," defeats every `^`-anchored template downstream, both parse
|
|
183
|
+
* strategies and lane-local regexes alike) — the same species of
|
|
184
|
+
* leftover-debris bug the preamble frames elsewhere in this file
|
|
185
|
+
* (GREETING_PREAMBLE_RE et al.) already avoid by consuming their own
|
|
186
|
+
* delimiter. The trailing `,?` is safe to make unconditional (unlike the
|
|
187
|
+
* preamble frames, no delimiter-required gate is needed): it only ever
|
|
188
|
+
* fires immediately after a MATCHED filler word, so it can only ever eat a
|
|
189
|
+
* comma that was already glued to filler, never a comma separating real
|
|
190
|
+
* content ("the modules, and the classes" — that comma sits after the
|
|
191
|
+
* content word "modules", not after any filler word). Pure, idempotent;
|
|
192
|
+
* unmatched text passes through byte-unchanged. */
|
|
193
|
+
export function stripFillerWords(text) {
|
|
194
|
+
let q = String(text || "");
|
|
195
|
+
if (FILLER_RE) q = q.replace(FILLER_RE, " ");
|
|
196
|
+
return q.replace(/\s+/g, " ").trim();
|
|
197
|
+
}
|
|
198
|
+
|
|
155
199
|
// ---- closed PREAMBLE frames (0.8.2 feel wave, PLAN_CHAT_FEEL item 2) — the
|
|
156
200
|
// conversational wrapping a developer puts AROUND a real question: a greeting
|
|
157
201
|
// lead-in with a delimiter ("hey there, quick question - …"), a thanks lead-in
|
|
@@ -634,13 +678,7 @@ export function normalizeQuery(text) {
|
|
|
634
678
|
// leaving punctuation/clause debris that poisons the parse.
|
|
635
679
|
q = applySubordinationFrames(q);
|
|
636
680
|
q = applyConditionalFrames(q);
|
|
637
|
-
|
|
638
|
-
const fillerRe = new RegExp(
|
|
639
|
-
"\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
|
|
640
|
-
"gi",
|
|
641
|
-
);
|
|
642
|
-
q = q.replace(fillerRe, " ");
|
|
643
|
-
}
|
|
681
|
+
q = stripFillerWords(q);
|
|
644
682
|
// emphatic trailing punctuation (item 10): a run of terminal "?" collapses to
|
|
645
683
|
// one — the anchored templates consume exactly one optional trailing "?", so
|
|
646
684
|
// "…walk.mjs??" otherwise leaks a stray "?" into the captured object term (the
|
package/src/memory/core.mjs
CHANGED
|
@@ -1162,6 +1162,18 @@ export function normFactTerm(t) {
|
|
|
1162
1162
|
// every seeded fact — the golden-equivalence test pins the two paths together.
|
|
1163
1163
|
const factIdFor = (s, p, o) => `fact:${fnv1aHex(`${s}\0${p}\0${o}`)}`;
|
|
1164
1164
|
|
|
1165
|
+
/** Content-address a fact's id from its (subject, predicate, object) WITHOUT
|
|
1166
|
+
* writing it — the SAME normalize+NUL-join+hash contract appendFact/
|
|
1167
|
+
* appendFacts use internally (factIdFor, above). Exposed so a caller that
|
|
1168
|
+
* needs to name a premise's or a not-yet-written conclusion's id (e.g.
|
|
1169
|
+
* syllogise.mjs's justification-tracking retraction machinery, PLAN_SYLLOGIST.md
|
|
1170
|
+
* §3) can compute it deterministically without an extra read — ids are
|
|
1171
|
+
* content-addressed, never sequence-assigned, so this is safe to call
|
|
1172
|
+
* before, instead of, or in place of an actual append. Pure, no I/O. */
|
|
1173
|
+
export function factIdForTriple(subject, predicate, object) {
|
|
1174
|
+
return factIdFor(normFactTerm(subject), normText(predicate), normFactTerm(object));
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1165
1177
|
/** Append one grammar-derived OWL triple, RDF-reified: a `Fact` individual
|
|
1166
1178
|
* carrying rdf:subject / rdf:predicate / rdf:object (+ provenance). The
|
|
1167
1179
|
* Phase-2 ACE parser's write point. Same (s,p,o) → same id → upsert, never a
|
|
@@ -1238,6 +1250,14 @@ export async function appendFact(dir, { subject, predicate, object, provenance =
|
|
|
1238
1250
|
* appendFact's own entailed-hook passthrough, batched: syllogise.mjs's
|
|
1239
1251
|
* materializing pass is this function's main caller, so this is the write
|
|
1240
1252
|
* path a rule's conclusion trust actually rides (recomputeFactTrust, above).
|
|
1253
|
+
* A fact may also carry `justification` (optional, array of premise fact
|
|
1254
|
+
* ids — PLAN_SYLLOGIST.md §3's persisted-justification step): stored
|
|
1255
|
+
* verbatim as `mgx:factJustification` (space-joined; fact ids never contain
|
|
1256
|
+
* a space), last-write-wins per id (a re-derivation via a DIFFERENT premise
|
|
1257
|
+
* pair, after the original was retracted and this fact re-earned its place
|
|
1258
|
+
* some other way, should overwrite the stale justification, not keep it) —
|
|
1259
|
+
* never written at all for a plain taught/asserted fact (`justification`
|
|
1260
|
+
* omitted), which stays byte-identical to before this field existed.
|
|
1241
1261
|
* Returns { ids, appended, skipped } — ids one per applied fact (in order),
|
|
1242
1262
|
* appended = ids.length, skipped = malformed count. */
|
|
1243
1263
|
export async function appendFacts(dir, facts) {
|
|
@@ -1258,6 +1278,7 @@ export async function appendFacts(dir, facts) {
|
|
|
1258
1278
|
quantifier: normText(f?.quantifier),
|
|
1259
1279
|
premiseTrusts: Array.isArray(f?.premiseTrusts) ? f.premiseTrusts : undefined,
|
|
1260
1280
|
ruleConfidence: typeof f?.ruleConfidence === "number" ? f.ruleConfidence : undefined,
|
|
1281
|
+
justification: Array.isArray(f?.justification) ? f.justification.filter(Boolean) : undefined,
|
|
1261
1282
|
});
|
|
1262
1283
|
}
|
|
1263
1284
|
const ids = [];
|
|
@@ -1290,6 +1311,7 @@ export async function appendFacts(dir, facts) {
|
|
|
1290
1311
|
...(provs.length ? [{ prop: "mgx:factProvenance", key: "provenance", value: provs.join(" | ") }] : []),
|
|
1291
1312
|
...(f.tokens.length ? [{ prop: "mgx:hasProseTokens", key: "prose_tokens", value: f.tokens.join(" ") }] : []),
|
|
1292
1313
|
...(qVal ? [{ prop: "mgx:factQuantifier", key: "quantifier", value: qVal }] : []),
|
|
1314
|
+
...(f.justification && f.justification.length ? [{ prop: "mgx:factJustification", key: "justification", value: f.justification.join(" ") }] : []),
|
|
1293
1315
|
],
|
|
1294
1316
|
};
|
|
1295
1317
|
// Upsert into BOTH the array (replace-in-place keeps order) and the index.
|
|
@@ -1664,6 +1686,7 @@ export function readFactRows(memory) {
|
|
|
1664
1686
|
const sourceTypes = sourceIds
|
|
1665
1687
|
.map((id) => (sourcesById.get(id)?.attributes || []).find((a) => a?.prop === "mgx:sourceType")?.value)
|
|
1666
1688
|
.filter(Boolean);
|
|
1689
|
+
const justificationRaw = get("justification");
|
|
1667
1690
|
rows.push({
|
|
1668
1691
|
id: ind.id,
|
|
1669
1692
|
subject: get("subject"), predicate: get("predicate"), object: get("object"),
|
|
@@ -1671,11 +1694,51 @@ export function readFactRows(memory) {
|
|
|
1671
1694
|
quantifier: get("quantifier"), // "" unless a plural class-membership teach set one (Feature A pt.3)
|
|
1672
1695
|
sourceIds, sourceTypes,
|
|
1673
1696
|
trust: Number((ind.attributes || []).find((a) => a?.prop === TRUST_SCORE_PROP)?.value) || 0,
|
|
1697
|
+
// [] unless a rule persisted its premise fact ids (PLAN_SYLLOGIST.md §3's
|
|
1698
|
+
// justification-tracking step — scm-sco only, today; see syllogise.mjs).
|
|
1699
|
+
justification: justificationRaw ? justificationRaw.split(" ").filter(Boolean) : [],
|
|
1674
1700
|
});
|
|
1675
1701
|
}
|
|
1676
1702
|
return rows;
|
|
1677
1703
|
}
|
|
1678
1704
|
|
|
1705
|
+
/**
|
|
1706
|
+
* Retract Fact individuals by id — a real DELETE, the mechanism `syllogise.mjs`'s
|
|
1707
|
+
* own header comment has always PROMISED ("fully RETRACTABLE by provenance when
|
|
1708
|
+
* the source graph moves") but that, until PLAN_SYLLOGIST.md §3's retraction
|
|
1709
|
+
* build, nothing in this file actually implemented: un-believing something used
|
|
1710
|
+
* to mean re-running the whole batch pass and hoping dedup naturally sorted it
|
|
1711
|
+
* out, with no targeted removal at all. Drops each matching Fact individual and
|
|
1712
|
+
* scrubs any edge group (`statedBy`, etc.) that referenced it as subject OR
|
|
1713
|
+
* object, so no dangling edge survives the delete — then recounts classes once.
|
|
1714
|
+
* A Source left with zero remaining statedBy edges is NOT itself deleted (an
|
|
1715
|
+
* orphaned Source individual is harmless — it materialises nothing and costs
|
|
1716
|
+
* nothing to leave — so this stays a pure, minimal retraction, not a GC pass).
|
|
1717
|
+
* Ids that don't resolve to a live Fact are silently skipped (an idempotent,
|
|
1718
|
+
* honest no-op — never an error: a caller may retry a retraction against a
|
|
1719
|
+
* concurrently-mutated store). Returns { removed } — the ids ACTUALLY deleted,
|
|
1720
|
+
* a possibly-smaller set than the input. */
|
|
1721
|
+
export async function removeFacts(dir, ids) {
|
|
1722
|
+
const idSet = new Set((ids || []).filter(Boolean));
|
|
1723
|
+
const removed = [];
|
|
1724
|
+
if (!idSet.size) return { removed };
|
|
1725
|
+
await mutateMemory(dir, (payload) => {
|
|
1726
|
+
payload.individuals = (payload.individuals || []).filter((ind) => {
|
|
1727
|
+
if (ind?.class === FACT_CLASS && idSet.has(ind.id)) { removed.push(ind.id); return false; }
|
|
1728
|
+
return true;
|
|
1729
|
+
});
|
|
1730
|
+
if (!removed.length) return; // honest no-op — nothing matched, no write needed beyond this
|
|
1731
|
+
const removedSet = new Set(removed);
|
|
1732
|
+
for (const group of payload.objectProperties || []) {
|
|
1733
|
+
const before = group.examples || [];
|
|
1734
|
+
group.examples = before.filter((e) => !removedSet.has(e?.subject) && !removedSet.has(e?.object));
|
|
1735
|
+
group.count = group.examples.length;
|
|
1736
|
+
}
|
|
1737
|
+
recountClasses(payload);
|
|
1738
|
+
});
|
|
1739
|
+
return { removed };
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1679
1742
|
/** The trust floor a fact must clear before a differing object counts as a real
|
|
1680
1743
|
* contradiction (below it the fact is too weak to contradict anything). */
|
|
1681
1744
|
export const CONTRADICTION_TRUST_FLOOR = 0.5;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// paraphrase.mjs — PLAN_BREADTH_FIRST_NLU.md (c) / ROADMAP.md "Ambition":
|
|
2
|
+
// "Paraphrase alongside the original, verified, never instead of it." A
|
|
3
|
+
// surface-realization variant of an isa-family (`rdfs:subClassOf`) teach
|
|
4
|
+
// confirmation, shown NEXT TO the original literal confirmation, never
|
|
5
|
+
// replacing it — and never shown at all unless its accuracy is checked by
|
|
6
|
+
// running tmct's OWN deterministic inference machinery (`src/syllogise.mjs`)
|
|
7
|
+
// against both the original and the paraphrase.
|
|
8
|
+
//
|
|
9
|
+
// Scope, deliberately narrow: isa-family (`rdfs:subClassOf`) facts only — the
|
|
10
|
+
// one predicate family `syllogise.mjs`'s `deriveSubClassClosure` actually
|
|
11
|
+
// reasons over. tmct's other taught predicate families (someValuesFrom,
|
|
12
|
+
// disjointWith, cardinality) have their own entailment rules in syllogise.mjs
|
|
13
|
+
// too, but this pass covers the single most common teach shape ("X is a kind
|
|
14
|
+
// of Y") the plan's own worked example (ROADMAP.md's Ambition section, the
|
|
15
|
+
// `PLAN_BREADTH_FIRST_NLU.md` §8 canonical example) already anchors on;
|
|
16
|
+
// widening to the other predicate families is a natural, separately-scoped
|
|
17
|
+
// follow-on once this shape is proven live.
|
|
18
|
+
//
|
|
19
|
+
// The generator and the recognizer are a MATCHED PAIR by construction — every
|
|
20
|
+
// template `paraphraseSubClass` can produce has a corresponding branch in
|
|
21
|
+
// `recoverSubClassTriple` that parses it back to exactly the same
|
|
22
|
+
// {subject, object} pair. This is a CLOSED set (never open-ended NLP), so
|
|
23
|
+
// recognition is exact, not fuzzy. "Verified" means: re-derive the
|
|
24
|
+
// `rdfs:subClassOf` transitive closure (the real conclusions this fact would
|
|
25
|
+
// license, via `deriveSubClassClosure`) once seeded with the ORIGINAL triple
|
|
26
|
+
// and once seeded with the triple RECOVERED FROM the paraphrase, over the
|
|
27
|
+
// SAME existing taught edges — the two closures must be identical (both
|
|
28
|
+
// derived edge SETS byte-for-byte equal). A generator bug that silently
|
|
29
|
+
// swapped subject/object (a real risk for a passive-voice template) would be
|
|
30
|
+
// caught here even in a graph with no other taught facts at all, because a
|
|
31
|
+
// swapped pair changes the closure's own subject/object roles the moment any
|
|
32
|
+
// OTHER edge touches either term — exactly the "must entail the same
|
|
33
|
+
// conclusions, neither may contradict the other" check the Ambition asks for,
|
|
34
|
+
// not a shallower string-equality stand-in for it.
|
|
35
|
+
|
|
36
|
+
import { deriveSubClassClosure } from "./syllogise.mjs";
|
|
37
|
+
import { normFactTerm } from "./memory/core.mjs";
|
|
38
|
+
import { fnv1aHex } from "./hash.mjs";
|
|
39
|
+
|
|
40
|
+
// Every template reads "SUBJECT ⊑ OBJECT" left to right — no passive/reordered
|
|
41
|
+
// form is offered, since a reordered form is exactly the shape most likely to
|
|
42
|
+
// invert subject/object under a naive regex recognizer; keeping the pair's
|
|
43
|
+
// left-to-right order fixed across every template keeps the recognizer trivial
|
|
44
|
+
// AND correct by construction, rather than needing the closure check to catch
|
|
45
|
+
// a bug the generator could have avoided entirely.
|
|
46
|
+
const articleFor = (word) => (/^[aeiou]/i.test(String(word || "")) ? "an" : "a");
|
|
47
|
+
const SUBCLASS_TEMPLATES = [
|
|
48
|
+
(s, o) => `${s} is a kind of ${o}`,
|
|
49
|
+
(s, o) => `${s} is a type of ${o}`,
|
|
50
|
+
(s, o) => `every ${s} is ${articleFor(o)} ${o}`,
|
|
51
|
+
(s, o) => `${s} counts as ${articleFor(o)} ${o}`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// One regex per template above, in the SAME order — deliberately paired by
|
|
55
|
+
// index rather than derived/inverted from the generator, so a change to one
|
|
56
|
+
// side can never silently desync from the other without a test catching it
|
|
57
|
+
// (see paraphrase.test.mjs's own round-trip check over every template).
|
|
58
|
+
const SUBCLASS_RECOGNIZERS = [
|
|
59
|
+
/^(.+?)\s+is\s+a\s+kind\s+of\s+(.+)$/i,
|
|
60
|
+
/^(.+?)\s+is\s+a\s+type\s+of\s+(.+)$/i,
|
|
61
|
+
/^every\s+(.+?)\s+is\s+an?\s+(.+)$/i,
|
|
62
|
+
/^(.+?)\s+counts\s+as\s+an?\s+(.+)$/i,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** Deterministic template pick — same (subject, object) always picks the same
|
|
66
|
+
* template (pinnable in tests), spread across the table by a pure hash, never
|
|
67
|
+
* Math.random/Date.now (same discipline as answer-variants.mjs's pickPhrase). */
|
|
68
|
+
function pickTemplateIndex(subject, object) {
|
|
69
|
+
const h = fnv1aHex(`${subject}\0${object}`);
|
|
70
|
+
return parseInt(h.slice(0, 8), 16) % SUBCLASS_TEMPLATES.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Generate a paraphrase of the `rdfs:subClassOf` confirmation "SUBJECT is a
|
|
74
|
+
* kind of OBJECT" for the given (already-normalized-or-not) subject/object —
|
|
75
|
+
* never null, always one of the closed templates. Rule/template-based only,
|
|
76
|
+
* no LLM, matching every other generator in this product. */
|
|
77
|
+
export function paraphraseSubClass(subject, object) {
|
|
78
|
+
const idx = pickTemplateIndex(subject, object);
|
|
79
|
+
return SUBCLASS_TEMPLATES[idx](subject, object);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Recover {subject, object} from a paraphrase this module itself generated
|
|
83
|
+
* (or any text matching one of the closed templates) — null if it matches
|
|
84
|
+
* none of them. Never a fuzzy/NLP parse; a plain closed-set regex match, the
|
|
85
|
+
* exact inverse of paraphraseSubClass by construction. */
|
|
86
|
+
export function recoverSubClassTriple(text) {
|
|
87
|
+
const s = String(text || "").trim();
|
|
88
|
+
for (const re of SUBCLASS_RECOGNIZERS) {
|
|
89
|
+
const m = s.match(re);
|
|
90
|
+
if (m) return { subject: m[1].trim(), object: m[2].trim() };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The actual verification step: does re-deriving the rdfs:subClassOf closure
|
|
96
|
+
* from the paraphrase's own recovered triple produce the SAME entailed edge
|
|
97
|
+
* set as deriving it from the original triple, over the same pre-existing
|
|
98
|
+
* taught edges? `existingEdges` is `[[a,b], …]` — the SAME already-normalized
|
|
99
|
+
* pair-list shape `deriveSubClassClosure` itself takes (subClassEdges read
|
|
100
|
+
* straight off taught Fact rows elsewhere in this codebase). Returns
|
|
101
|
+
* `{verified, closure}` — `closure` is the original triple's own derived
|
|
102
|
+
* conclusions (handed back so a caller can show/log them), `verified` is
|
|
103
|
+
* false when the paraphrase's recovered triple doesn't reparse, doesn't match
|
|
104
|
+
* the original's normalized (subject, object), or derives a different
|
|
105
|
+
* closure — any one of those means the paraphrase must NOT be shown. */
|
|
106
|
+
export function verifySubClassParaphrase(subject, object, paraphraseText, existingEdges = []) {
|
|
107
|
+
const origSubj = normFactTerm(subject);
|
|
108
|
+
const origObj = normFactTerm(object);
|
|
109
|
+
const recovered = recoverSubClassTriple(paraphraseText);
|
|
110
|
+
const closureOf = (a, b) => deriveSubClassClosure([...existingEdges, [a, b]]);
|
|
111
|
+
const keyOf = (edges) => edges.map((e) => `${e.subject}\0${e.object}`).sort().join("|");
|
|
112
|
+
const closure = closureOf(origSubj, origObj);
|
|
113
|
+
if (!recovered) return { verified: false, closure };
|
|
114
|
+
const recSubj = normFactTerm(recovered.subject);
|
|
115
|
+
const recObj = normFactTerm(recovered.object);
|
|
116
|
+
if (recSubj !== origSubj || recObj !== origObj) return { verified: false, closure };
|
|
117
|
+
const paraphraseClosure = closureOf(recSubj, recObj);
|
|
118
|
+
const verified = keyOf(closure) === keyOf(paraphraseClosure);
|
|
119
|
+
return { verified, closure };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Top-level convenience: generate + verify in one call. Returns the
|
|
123
|
+
* paraphrase text ONLY when verified (never an unverified paraphrase);
|
|
124
|
+
* `null` otherwise — the caller shows nothing extra rather than something
|
|
125
|
+
* unchecked, keeping "verified, never instead of the original" strict even
|
|
126
|
+
* on a genuine (unexpected) verification failure. */
|
|
127
|
+
export function paraphraseVerifiedSubClass(subject, object, existingEdges = []) {
|
|
128
|
+
const text = paraphraseSubClass(subject, object);
|
|
129
|
+
const { verified } = verifySubClassParaphrase(subject, object, text, existingEdges);
|
|
130
|
+
return verified ? text : null;
|
|
131
|
+
}
|
package/src/router/planner.mjs
CHANGED
|
@@ -25,10 +25,6 @@
|
|
|
25
25
|
// chain. Bounded depth + a hard step counter GUARANTEE termination — no
|
|
26
26
|
// unbounded search can ever wedge the caller (the harness also caps us).
|
|
27
27
|
//
|
|
28
|
-
// THE OPEN-WORLD BOUNDARY, named honestly: novelty the declared methods + operators
|
|
29
|
-
// do not cover (a sub-goal that resolves to nothing, a connective we do not model)
|
|
30
|
-
// is REFUSED/ESCALATED, not guessed. Sound/complete is claimed only INSIDE the
|
|
31
|
-
// declared world.
|
|
32
28
|
|
|
33
29
|
import { resolveOne, extractEntity } from "./resolver.mjs";
|
|
34
30
|
|
package/src/server.mjs
CHANGED
|
@@ -162,7 +162,14 @@ export const TOOLS = [
|
|
|
162
162
|
},
|
|
163
163
|
];
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
// Exported (was module-private) so chat.mjs's compare lane can load the SAME
|
|
166
|
+
// graph dispatchTool's own tools load — no new loading path, just direct reuse
|
|
167
|
+
// of the existing config -> source.fetchEntities -> parseEntities chain, for
|
|
168
|
+
// the case where runAsk's own `graph` param is null (the common case; it's
|
|
169
|
+
// only preloaded when a caller already has one in hand — see runAsk's own
|
|
170
|
+
// `if (graph && ...)` / dispatchTool("tmct_ask", …) split just above the
|
|
171
|
+
// compare lane's call site for the existing precedent).
|
|
172
|
+
export async function loadGraph(config, source) {
|
|
166
173
|
const payload = await source.fetchEntities(config);
|
|
167
174
|
const graph = parseEntities(payload);
|
|
168
175
|
if (!graph.individuals.length) {
|
package/src/source-slice.mjs
CHANGED
|
@@ -54,8 +54,7 @@ export async function readSpanSafe({ readFile, repoRoot, path, start, end, maxLi
|
|
|
54
54
|
// Normalize repoRoot to absolute here too (defense in depth) — resolve(repoRoot, path)
|
|
55
55
|
// is always absolute, so comparing it against a RELATIVE repoRoot would make this guard
|
|
56
56
|
// reject every read, not just traversal attempts (the actual bug this normalization
|
|
57
|
-
// fixes; callers should already pass an absolute repoRoot via src/config.mjs
|
|
58
|
-
// function is the real security boundary and must not depend on that).
|
|
57
|
+
// fixes; callers should already pass an absolute repoRoot via src/config.mjs.
|
|
59
58
|
const root = resolve(repoRoot);
|
|
60
59
|
const resolved = resolve(root, path);
|
|
61
60
|
if (resolved !== root && !resolved.startsWith(root + sep)) {
|
package/src/syllogise.mjs
CHANGED
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
// exist, so a general "no" is provable without needing one
|
|
102
102
|
// (`proveMaxCardinalityZeroDenial`).
|
|
103
103
|
|
|
104
|
-
import { loadMemory, appendFacts, readFactRows, normFactTerm } from "./memory/core.mjs";
|
|
104
|
+
import { loadMemory, appendFacts, readFactRows, normFactTerm, factIdForTriple, removeFacts } from "./memory/core.mjs";
|
|
105
105
|
|
|
106
106
|
/** scm-sco: the subClassOf-transitivity rule, and the provenance tag its
|
|
107
107
|
* conclusions carry. */
|
|
@@ -1035,6 +1035,18 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
1035
1035
|
...scmDerived.map((d) => ({
|
|
1036
1036
|
subject: d.subject, predicate: SUBCLASS_PREDICATE, object: d.object,
|
|
1037
1037
|
provenance: ENTAILED_PROVENANCE,
|
|
1038
|
+
// PLAN_SYLLOGIST.md §3's persisted-justification step, scm-sco only: the
|
|
1039
|
+
// two premise fact ids THIS conclusion actually rode (a⊑b, b⊑c) — ids
|
|
1040
|
+
// are content-addressed (factIdForTriple/memory/core.mjs), so this works
|
|
1041
|
+
// whether the premise is a stated fact or another entailment this SAME
|
|
1042
|
+
// pass just derived a round earlier (its id is predictable before it's
|
|
1043
|
+
// even written). Read back by retractSubClassOf (below) to find every
|
|
1044
|
+
// entailment a retracted premise could have supported, without a
|
|
1045
|
+
// whole-graph re-scan.
|
|
1046
|
+
justification: [
|
|
1047
|
+
factIdForTriple(d.subject, SUBCLASS_PREDICATE, d.via),
|
|
1048
|
+
factIdForTriple(d.via, SUBCLASS_PREDICATE, d.object),
|
|
1049
|
+
],
|
|
1038
1050
|
})),
|
|
1039
1051
|
...caxDerived.map((d) => ({
|
|
1040
1052
|
subject: d.subject, predicate: TYPE_PREDICATE, object: d.object,
|
|
@@ -1137,6 +1149,153 @@ export async function syllogise(repoDir, { depth = 32, budget = 50, focus = null
|
|
|
1137
1149
|
return { derived: written, count: written.length, budget, depth, truncated: written.length >= budget };
|
|
1138
1150
|
}
|
|
1139
1151
|
|
|
1152
|
+
/** True when EVERY provenance tag on a fact's (possibly " | "-joined) union is
|
|
1153
|
+
* an `entailed:*` tag — i.e. the fact has never been independently stated or
|
|
1154
|
+
* taught, only ever derived. A fact first entailed, then LATER also directly
|
|
1155
|
+
* taught (same (s,p,o) → same id → provenance union, appendFact's own upsert
|
|
1156
|
+
* contract), is NOT purely entailed any more — `retractSubClassOf`'s cascade
|
|
1157
|
+
* must never delete it just because its now-stale justification broke; the
|
|
1158
|
+
* taught half is a real, independent reason to keep believing it (the trust-
|
|
1159
|
+
* tier concern PLAN_SYLLOGIST.md §3 names: "must never touch a higher-trust
|
|
1160
|
+
* taught-only derivation"). */
|
|
1161
|
+
function isPurelyEntailed(provenance) {
|
|
1162
|
+
const tags = String(provenance || "").split(" | ").filter(Boolean);
|
|
1163
|
+
return tags.length > 0 && tags.every((t) => t.startsWith("entailed:"));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* PLAN_SYLLOGIST.md §3's first real, scoped retraction slice: JTMS-style
|
|
1168
|
+
* dependency-directed removal, for scm-sco ONLY (subClassOf transitivity —
|
|
1169
|
+
* this file's simplest rule, and §3's own worked example: "a premise later
|
|
1170
|
+
* disappears, what else must be un-believed?"). Deliberately NOT the fuller
|
|
1171
|
+
* ATMS §3 also sketches (tracking every alternate premise-SET per fact, "a
|
|
1172
|
+
* further, NOT-currently-planned step") — this tracks exactly ONE
|
|
1173
|
+
* justification per entailed fact (the premise pair it actually rode, set at
|
|
1174
|
+
* write time — syllogise()'s own toWrite mapping, above): the JTMS-shaped
|
|
1175
|
+
* step §3 names as missing today ("a JTMS-shaped single justification per
|
|
1176
|
+
* fact in spirit, though not yet a persisted, walkable one").
|
|
1177
|
+
*
|
|
1178
|
+
* Retracting `subject ⊑ object` (a STATED or a previously-ENTAILED fact —
|
|
1179
|
+
* either may be retracted) proceeds in bounded rounds:
|
|
1180
|
+
* 1. Remove the named fact.
|
|
1181
|
+
* 2. Scan stored entailed scm-sco facts (purely-entailed ones only —
|
|
1182
|
+
* `isPurelyEntailed`, above) for any whose persisted justification cites
|
|
1183
|
+
* an id removed so far — candidates.
|
|
1184
|
+
* 3. VERIFY, never assume: a candidate is removed only if `subject ⊑
|
|
1185
|
+
* object` is NO LONGER reachable over the SURVIVING subClassOf edge set
|
|
1186
|
+
* (a full ⊑-ancestor walk, `buildAncestorCloser` — the SAME shared
|
|
1187
|
+
* machinery `deriveTypePropagation`/`deriveDisjointViolations` already
|
|
1188
|
+
* reuse, not reimplemented here). A fact with a SECOND, independent
|
|
1189
|
+
* derivation path survives — a real possibility scm-sco's transitive
|
|
1190
|
+
* closure allows (a⊑b⊑d AND a⊑c⊑d both license a⊑d) — exactly the
|
|
1191
|
+
* failure mode a bare "delete anything citing the retracted id" JTMS
|
|
1192
|
+
* walk gets wrong, and precisely why de Kleer's ATMS exists at all (§3's
|
|
1193
|
+
* own citation). This VERIFY step is this slice's cheap, bounded answer
|
|
1194
|
+
* to that known JTMS over-retraction limitation: one local graph walk
|
|
1195
|
+
* per candidate, never a full alternate-justification enumeration.
|
|
1196
|
+
* 4. Repeat: a fact confirmed-removed this round becomes a new cascade
|
|
1197
|
+
* source for the next round (removing a mid-chain link can ripple).
|
|
1198
|
+
*
|
|
1199
|
+
* Bounded by `budget` (max facts examined+removed, default 50 — the SAME
|
|
1200
|
+
* default every other rule in this file uses) and `depth` (max cascade
|
|
1201
|
+
* rounds, default 32, mirroring `deriveSubClassClosure`'s own fixpoint cap).
|
|
1202
|
+
* `truncated` flags the cascade may have been cut short before reaching a
|
|
1203
|
+
* fixpoint (candidates still pending when budget/depth ran out) — the SAME
|
|
1204
|
+
* honest-signal discipline `syllogise()`'s own `truncated` flag follows: a
|
|
1205
|
+
* caller must not read a truncated cascade's survivors as "provably still
|
|
1206
|
+
* consistent," only as "not yet shown inconsistent within budget."
|
|
1207
|
+
*
|
|
1208
|
+
* Known, DELIBERATE scope limit (not a bug): this only ever touches scm-sco's
|
|
1209
|
+
* own entailed subClassOf facts. The other four rules (cax-sco/cax-dw/
|
|
1210
|
+
* cls-svf1/scm-svf1) do not yet persist a justification (none call
|
|
1211
|
+
* `factIdForTriple`/write `justification` — only `syllogise()`'s scmDerived
|
|
1212
|
+
* mapping does, above), so a type/disjointWith/someValuesFrom conclusion that
|
|
1213
|
+
* ALSO went stale when this same premise was retracted is not cascaded here.
|
|
1214
|
+
* Extending justification-tracking to the other four rules is mechanical
|
|
1215
|
+
* (each already computes a `via`/`viaX` pivot) but is a separate follow-up,
|
|
1216
|
+
* not attempted in this slice.
|
|
1217
|
+
*
|
|
1218
|
+
* Returns { retracted, count, budget, depth, truncated, found } — `retracted`
|
|
1219
|
+
* is every id actually removed (target first, then cascade order); `found`
|
|
1220
|
+
* is false (nothing else meaningful) when `subject ⊑ object` was never a
|
|
1221
|
+
* stored fact at all — an honest no-op, matching this module's "never guess"
|
|
1222
|
+
* discipline. No I/O beyond the one `removeFacts` call (skipped entirely when
|
|
1223
|
+
* `found` is false).
|
|
1224
|
+
*/
|
|
1225
|
+
export async function retractSubClassOf(repoDir, subject, object, { budget = 50, depth = 32 } = {}) {
|
|
1226
|
+
const s = normFactTerm(subject);
|
|
1227
|
+
const o = normFactTerm(object);
|
|
1228
|
+
const targetId = factIdForTriple(s, SUBCLASS_PREDICATE, o);
|
|
1229
|
+
const memory = await loadMemory(repoDir);
|
|
1230
|
+
const rows = readFactRows(memory);
|
|
1231
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
1232
|
+
if (!byId.has(targetId)) return { retracted: [], count: 0, budget, depth, truncated: false, found: false };
|
|
1233
|
+
|
|
1234
|
+
// The FULL current subClassOf edge set (stated + every prior entailment) —
|
|
1235
|
+
// the working graph this function's VERIFY step walks each round; a
|
|
1236
|
+
// removed id's own edge is excluded from that round's walk onward.
|
|
1237
|
+
const scRows = rows.filter((r) => isSubClassOf(r.predicate));
|
|
1238
|
+
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.
|
|
1244
|
+
const entailedScRows = scRows.filter((r) => r.justification.length && isPurelyEntailed(r.provenance));
|
|
1245
|
+
|
|
1246
|
+
const removed = new Set([targetId]);
|
|
1247
|
+
const order = [targetId]; // deterministic report order: target first, then removal order
|
|
1248
|
+
let truncated = false;
|
|
1249
|
+
let round = 0;
|
|
1250
|
+
for (; round < depth; round += 1) {
|
|
1251
|
+
const candidates = entailedScRows
|
|
1252
|
+
.filter((r) => !removed.has(r.id) && r.justification.some((j) => removed.has(j)))
|
|
1253
|
+
.sort((a, b) => a.subject.localeCompare(b.subject) || a.object.localeCompare(b.object));
|
|
1254
|
+
if (!candidates.length) break; // fixpoint — nothing left to (re-)check
|
|
1255
|
+
|
|
1256
|
+
// The surviving edge set for THIS round's verify walk — DRed's own
|
|
1257
|
+
// "delete a superset, then selectively rederive" discipline (§3's own
|
|
1258
|
+
// citation, Gupta/Mumick/Subrahmanian 1993): every candidate's OWN edge
|
|
1259
|
+
// is excluded too, alongside every OTHER candidate under suspicion this
|
|
1260
|
+
// SAME round, not just `removed` — otherwise a candidate would trivially
|
|
1261
|
+
// "reach itself" through its own not-yet-deleted edge (or lean on a
|
|
1262
|
+
// sibling candidate that is itself only standing on the same broken
|
|
1263
|
+
// premise), understating what actually still needs re-verifying. A
|
|
1264
|
+
// candidate that reaches its target through some OTHER, untouched edge
|
|
1265
|
+
// (a genuinely independent derivation path this fact's single persisted
|
|
1266
|
+
// justification never recorded) correctly survives.
|
|
1267
|
+
const candidateIds = new Set(candidates.map((c) => c.id));
|
|
1268
|
+
const survivingEdges = [...edgeOf.entries()]
|
|
1269
|
+
.filter(([id]) => !removed.has(id) && !candidateIds.has(id))
|
|
1270
|
+
.map(([, e]) => e);
|
|
1271
|
+
const ancestorsOf = buildAncestorCloser(survivingEdges);
|
|
1272
|
+
|
|
1273
|
+
let progressed = false;
|
|
1274
|
+
let hitBudget = false;
|
|
1275
|
+
for (const c of candidates) {
|
|
1276
|
+
if (removed.size >= budget) { hitBudget = true; break; }
|
|
1277
|
+
// does subject⊑object still hold WITHOUT the retracted premise, via ANY
|
|
1278
|
+
// surviving path (not just the one this fact was originally derived
|
|
1279
|
+
// through)? A survivor keeps its (now possibly re-groundable, still
|
|
1280
|
+
// TRUE) fact and is never re-examined again this call.
|
|
1281
|
+
if (ancestorsOf(c.subject).has(c.object)) continue; // a second, independent path still supports it — keep
|
|
1282
|
+
removed.add(c.id);
|
|
1283
|
+
order.push(c.id);
|
|
1284
|
+
progressed = true;
|
|
1285
|
+
}
|
|
1286
|
+
if (hitBudget) { truncated = true; break; }
|
|
1287
|
+
if (!progressed) break; // every candidate this round survived verification — fixpoint
|
|
1288
|
+
}
|
|
1289
|
+
if (!truncated && round >= depth) {
|
|
1290
|
+
// depth exhausted, not a natural fixpoint — honestly flag it if a
|
|
1291
|
+
// pending candidate would still have been checked next round.
|
|
1292
|
+
truncated = entailedScRows.some((r) => !removed.has(r.id) && r.justification.some((j) => removed.has(j)));
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const { removed: actuallyRemoved } = await removeFacts(repoDir, order);
|
|
1296
|
+
return { retracted: actuallyRemoved, count: actuallyRemoved.length, budget, depth, truncated, found: true };
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1140
1299
|
/**
|
|
1141
1300
|
* PROOF SEARCH (not a third rule — a bounded ROOTED chase that composes the
|
|
1142
1301
|
* two rules above for a single "does `subj` reach one of `targets`?" query,
|