@polycode-projects/the-mechanical-code-talker 6.0.17 → 6.0.19
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/package.json +3 -1
- package/src/adapters/corpus/research-source.mjs +6 -2
- package/src/adapters/corpus/wikidata-live.mjs +92 -51
- package/src/adapters/memory/core.mjs +53 -5
- package/src/adapters/memory/rows.mjs +253 -21
- package/src/domain/news-feed.mjs +790 -75
- package/src/domain/sense-gate.mjs +220 -0
- package/src/domain/syllogise.mjs +39 -8
- package/src/domain/term-ledger.mjs +16 -1
- package/src/services/chat.mjs +17 -12
- package/src/services/extract-facts.mjs +284 -19
- package/src/services/news-viz.mjs +31 -0
- package/src/services/news.mjs +67 -14
- package/src/surfaces/web/memory-ask-browser.bundle.js +142 -142
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// sense-gate.mjs — the disjointness gate on isa DERIVATION.
|
|
2
|
+
//
|
|
3
|
+
// WordNet-derived bands flatten every sense of a word onto one label. "region"
|
|
4
|
+
// carries a geographic sense (region ⊑ location) and an anatomical one
|
|
5
|
+
// (region ⊑ body part), and both rows store the same six characters. Each row
|
|
6
|
+
// is true of its own sense, so the corpus is right to hold both. subClassOf
|
|
7
|
+
// transitivity then walks straight across the join:
|
|
8
|
+
// russia ⊑ country ⊑ geographical area ⊑ region ⊑ body part.
|
|
9
|
+
//
|
|
10
|
+
// This module refuses that step, and only that step. It answers one question,
|
|
11
|
+
// `declines(subject, object)`, for a candidate DERIVED isa edge. An asserted
|
|
12
|
+
// row never reaches it: the gate constrains what the closure concludes, never
|
|
13
|
+
// what a corpus or a teacher records. A refusal costs the MATERIALISED
|
|
14
|
+
// shortcut row and nothing else. The stated chain stays whole, and every
|
|
15
|
+
// walker over it (findIsaChain, the ancestor closers) still reaches what it
|
|
16
|
+
// reached before.
|
|
17
|
+
//
|
|
18
|
+
// How a term is placed. Every candidate is resolved to the top classes it
|
|
19
|
+
// sits under, by an upward breadth-first walk over the ASSERTED isa edges,
|
|
20
|
+
// stopping at the FIRST level where any top appears and returning every top
|
|
21
|
+
// found at that level. Nearest-level, not full reachability: the graph these
|
|
22
|
+
// bands build is cyclic (place ⊑ passage ⊑ section ⊑ area ⊑ place is one of
|
|
23
|
+
// many), so "every top above x" resolves to nearly all of them for nearly
|
|
24
|
+
// every x and discriminates nothing. The nearest level is the sense the
|
|
25
|
+
// term's own short chain commits to. Russia reaches `place` in two hops and
|
|
26
|
+
// `body part` in four.
|
|
27
|
+
//
|
|
28
|
+
// When it declines. Both ends must resolve, the two top sets must not share a
|
|
29
|
+
// member, and EVERY cross pair must be a declared disjoint pair. One
|
|
30
|
+
// unresolved end, one shared top, or one pair the table does not separate,
|
|
31
|
+
// and the derivation goes through. A term genuinely sitting under two tops
|
|
32
|
+
// keeps both branches, so the gate cuts a crossing only when the evidence on
|
|
33
|
+
// both sides is unambiguous.
|
|
34
|
+
//
|
|
35
|
+
// Pure over the fact set. One memo per gate, no clock, no arrival order; the
|
|
36
|
+
// walk sorts every frontier by codepoint, so two ingestion orders of the same
|
|
37
|
+
// facts return the same verdicts.
|
|
38
|
+
|
|
39
|
+
import { normFactTerm } from "./hash.mjs";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The top classes the gate resolves a term to, each with the labels the
|
|
43
|
+
* committed bands actually spell it. A label is a synonym FOR the top, not a
|
|
44
|
+
* subclass of it: `location` names the same region of the ontology as
|
|
45
|
+
* `place`, so both spellings resolve to one top and never separate.
|
|
46
|
+
*
|
|
47
|
+
* `region` is deliberately absent. It is the sense-mixed node itself — making
|
|
48
|
+
* it a top would place `geographical area` under it and settle nothing.
|
|
49
|
+
*/
|
|
50
|
+
export const TOP_CLASSES = [
|
|
51
|
+
{ top: "place", labels: ["place", "location"] },
|
|
52
|
+
{
|
|
53
|
+
top: "body part",
|
|
54
|
+
labels: ["body part", "organ", "internal organ", "external body part", "body covering", "limb", "blood vessel"],
|
|
55
|
+
},
|
|
56
|
+
{ top: "living thing", labels: ["living thing", "organism"] },
|
|
57
|
+
{ top: "artifact", labels: ["artifact", "instrumentality"] },
|
|
58
|
+
{ top: "substance", labels: ["substance", "matter"] },
|
|
59
|
+
{ top: "event", labels: ["event", "happening"] },
|
|
60
|
+
{ top: "communication", labels: ["communication", "message"] },
|
|
61
|
+
{ top: "time period", labels: ["time period"] },
|
|
62
|
+
{ top: "feeling", labels: ["feeling", "emotion"] },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The pairs of tops that DO overlap, so the disjoint table can be every other
|
|
67
|
+
* pair. Each one names something the ontology really holds both ways: a
|
|
68
|
+
* cathedral is an artifact and a place; a book is an artifact and a
|
|
69
|
+
* communication; concrete is an artifact and a substance; bone is a body part
|
|
70
|
+
* and a substance; timber is a living thing and a substance; a speech is an
|
|
71
|
+
* event and a communication; a cry is a communication and a feeling.
|
|
72
|
+
*
|
|
73
|
+
* Listing the overlaps rather than the exclusions keeps the judgement small:
|
|
74
|
+
* nine tops make thirty-six pairs, and the seven below are the ones that need
|
|
75
|
+
* an argument. The rest follow.
|
|
76
|
+
*/
|
|
77
|
+
export const OVERLAPPING_TOP_PAIRS = [
|
|
78
|
+
["artifact", "communication"],
|
|
79
|
+
["artifact", "place"],
|
|
80
|
+
["artifact", "substance"],
|
|
81
|
+
["body part", "substance"],
|
|
82
|
+
["communication", "event"],
|
|
83
|
+
["communication", "feeling"],
|
|
84
|
+
["living thing", "substance"],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const pairKey = (a, b) => (a < b ? `${a}␟${b}` : `${b}␟${a}`);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Every disjoint pair of tops, as `[a, b]` with `a < b`, sorted — all pairs
|
|
91
|
+
* of `topClasses` except `overlappingPairs`. Derived rather than typed out,
|
|
92
|
+
* so the table is symmetric and duplicate-free by construction.
|
|
93
|
+
*/
|
|
94
|
+
export function disjointTopPairs(topClasses = TOP_CLASSES, overlappingPairs = OVERLAPPING_TOP_PAIRS) {
|
|
95
|
+
const overlaps = new Set((overlappingPairs || []).map(([a, b]) => pairKey(a, b)));
|
|
96
|
+
const tops = (topClasses || []).map((t) => t.top);
|
|
97
|
+
const pairs = [];
|
|
98
|
+
for (let i = 0; i < tops.length; i += 1) {
|
|
99
|
+
for (let j = i + 1; j < tops.length; j += 1) {
|
|
100
|
+
const [a, b] = tops[i] < tops[j] ? [tops[i], tops[j]] : [tops[j], tops[i]];
|
|
101
|
+
if (overlaps.has(pairKey(a, b))) continue;
|
|
102
|
+
pairs.push([a, b]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
pairs.sort((p, q) => (p[0] < q[0] ? -1 : p[0] > q[0] ? 1 : p[1] < q[1] ? -1 : p[1] > q[1] ? 1 : 0));
|
|
106
|
+
return pairs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** How far up the gate will look for a top before giving the term up as
|
|
110
|
+
* unplaced, and how many classes one term's walk may visit. Both bound the
|
|
111
|
+
* cost of a single `topsOf` on a graph whose upper levels fan out hard; a
|
|
112
|
+
* term whose nearest top sits past either bound is simply unplaced, and an
|
|
113
|
+
* unplaced term never blocks anything. */
|
|
114
|
+
export const DEFAULT_MAX_HOPS = 6;
|
|
115
|
+
export const DEFAULT_MAX_VISITED = 3000;
|
|
116
|
+
|
|
117
|
+
/** Codepoint order. Deliberately not localeCompare — the walk below must
|
|
118
|
+
* return the same set on any machine and any ICU version. */
|
|
119
|
+
const compareStrings = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds the gate over one fact set. `subClassEdges` and `typeEdges` are
|
|
123
|
+
* `[[child, parent], …]` lists of ASSERTED isa rows only (a caller holding
|
|
124
|
+
* entailed rows must filter them out first — feeding the closure's own output
|
|
125
|
+
* back in is what the gate exists to stop). Type edges act as a first hop, so
|
|
126
|
+
* an individual with a taught `rdf:type` places the same way a class does.
|
|
127
|
+
*
|
|
128
|
+
* Returns `{ declines, topsOf, tops, disjointPairs }`. `declines(subject,
|
|
129
|
+
* object)` is the whole contract; `topsOf(term)` is exposed for tests and for
|
|
130
|
+
* a caller that wants to explain a refusal.
|
|
131
|
+
*/
|
|
132
|
+
export function buildSenseGate({
|
|
133
|
+
subClassEdges = [], typeEdges = [], maxHops = DEFAULT_MAX_HOPS,
|
|
134
|
+
maxVisited = DEFAULT_MAX_VISITED, topClasses = TOP_CLASSES,
|
|
135
|
+
overlappingPairs = OVERLAPPING_TOP_PAIRS,
|
|
136
|
+
} = {}) {
|
|
137
|
+
const topOfLabel = new Map(); // normalized label -> its top's canonical name
|
|
138
|
+
for (const entry of topClasses || []) {
|
|
139
|
+
for (const label of entry.labels || []) {
|
|
140
|
+
const n = normFactTerm(label);
|
|
141
|
+
if (n) topOfLabel.set(n, entry.top);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const allTops = (topClasses || []).map((t) => t.top);
|
|
145
|
+
const pairs = disjointTopPairs(topClasses, overlappingPairs);
|
|
146
|
+
const disjoint = new Set(pairs.map(([a, b]) => pairKey(a, b)));
|
|
147
|
+
|
|
148
|
+
const parentSets = new Map(); // child -> Set(direct asserted superclass)
|
|
149
|
+
const addEdge = (child, parent) => {
|
|
150
|
+
if (!child || !parent || child === parent) return;
|
|
151
|
+
if (!parentSets.has(child)) parentSets.set(child, new Set());
|
|
152
|
+
parentSets.get(child).add(parent);
|
|
153
|
+
};
|
|
154
|
+
for (const [child, parent] of subClassEdges || []) addEdge(child, parent);
|
|
155
|
+
for (const [instance, cls] of typeEdges || []) addEdge(instance, cls);
|
|
156
|
+
// Sorted once, read many: `maxVisited` can truncate a level part-way, and
|
|
157
|
+
// truncating a Set in arrival order would make the verdict depend on which
|
|
158
|
+
// order the facts were ingested in.
|
|
159
|
+
const parents = new Map();
|
|
160
|
+
for (const [child, set] of parentSets) parents.set(child, [...set].sort(compareStrings));
|
|
161
|
+
|
|
162
|
+
const memo = new Map(); // term -> sorted array of top names at its nearest level
|
|
163
|
+
function topsOf(term) {
|
|
164
|
+
if (!term) return [];
|
|
165
|
+
if (memo.has(term)) return memo.get(term);
|
|
166
|
+
let found = [];
|
|
167
|
+
const own = topOfLabel.get(term);
|
|
168
|
+
if (own) {
|
|
169
|
+
found = [own];
|
|
170
|
+
} else {
|
|
171
|
+
const seen = new Set([term]);
|
|
172
|
+
let frontier = [term];
|
|
173
|
+
for (let hop = 1; hop <= maxHops && frontier.length && seen.size < maxVisited; hop += 1) {
|
|
174
|
+
const next = [];
|
|
175
|
+
for (const node of frontier) {
|
|
176
|
+
for (const parent of parents.get(node) || []) {
|
|
177
|
+
if (seen.has(parent)) continue;
|
|
178
|
+
seen.add(parent);
|
|
179
|
+
next.push(parent);
|
|
180
|
+
if (seen.size >= maxVisited) break;
|
|
181
|
+
}
|
|
182
|
+
if (seen.size >= maxVisited) break;
|
|
183
|
+
}
|
|
184
|
+
next.sort(compareStrings);
|
|
185
|
+
const hits = new Set();
|
|
186
|
+
for (const node of next) {
|
|
187
|
+
const top = topOfLabel.get(node);
|
|
188
|
+
if (top) hits.add(top);
|
|
189
|
+
}
|
|
190
|
+
if (hits.size) { found = [...hits].sort(compareStrings); break; }
|
|
191
|
+
frontier = next;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
memo.set(term, found);
|
|
195
|
+
return found;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** True when EVERY cross pair of the two top sets is a declared disjoint
|
|
199
|
+
* pair — one shared top or one undeclared pair and the answer is false. */
|
|
200
|
+
function separated(subjectTops, objectTops) {
|
|
201
|
+
for (const a of subjectTops) {
|
|
202
|
+
for (const b of objectTops) {
|
|
203
|
+
if (a === b) return false;
|
|
204
|
+
if (!disjoint.has(pairKey(a, b))) return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function declines(subject, object) {
|
|
211
|
+
if (!subject || !object || subject === object) return false;
|
|
212
|
+
const subjectTops = topsOf(subject);
|
|
213
|
+
if (!subjectTops.length) return false;
|
|
214
|
+
const objectTops = topsOf(object);
|
|
215
|
+
if (!objectTops.length) return false;
|
|
216
|
+
return separated(subjectTops, objectTops);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { declines, topsOf, tops: allTops, disjointPairs: pairs };
|
|
220
|
+
}
|
package/src/domain/syllogise.mjs
CHANGED
|
@@ -20,11 +20,17 @@
|
|
|
20
20
|
// `entailed:*` Source, prior 0.3, so it never outranks a stated fact and is
|
|
21
21
|
// retractable by provenance).
|
|
22
22
|
//
|
|
23
|
+
// The two isa rules take a fifth: the SENSE gate (sense-gate.mjs), which
|
|
24
|
+
// refuses a conclusion that only holds because a corpus band flattened two
|
|
25
|
+
// word senses onto one label — russia ⊑ … ⊑ region ⊑ body part. It screens
|
|
26
|
+
// derivations only; a stated fact is never blocked.
|
|
27
|
+
//
|
|
23
28
|
// Two further capabilities below are LIVE-CHASE ONLY, never part of the
|
|
24
29
|
// batch pass: cardinality monotonicity (`proveCardinalityAtLeast`) and
|
|
25
30
|
// max-cardinality-0 as encoded negation (`proveMaxCardinalityZeroDenial`).
|
|
26
31
|
|
|
27
32
|
import { normFactTerm, factIdForTriple } from "./hash.mjs";
|
|
33
|
+
import { buildSenseGate } from "./sense-gate.mjs";
|
|
28
34
|
|
|
29
35
|
/** The two persisting entry points (syllogise, retractSubClassOf) take the
|
|
30
36
|
* memory store's read/write functions through a required `store` option —
|
|
@@ -122,8 +128,14 @@ function normalizeFocus(focus) {
|
|
|
122
128
|
* (not already present), each `{ subject, object, via }`, bounded by `budget`
|
|
123
129
|
* and `depth`, focus-filtered, tautology- and dedup-screened, in a deterministic
|
|
124
130
|
* order. No I/O — this is the whole inference kernel, unit-testable in isolation.
|
|
131
|
+
*
|
|
132
|
+
* `gate` (sense-gate.mjs's `buildSenseGate`, or null) is the SENSE screen: a
|
|
133
|
+
* candidate a⊑c is dropped when the gate places a and c under top classes the
|
|
134
|
+
* ontology declares disjoint, so a band that flattens two word senses onto one
|
|
135
|
+
* label cannot license a walk across the join. Null (the default) leaves the
|
|
136
|
+
* kernel's behaviour exactly as it was.
|
|
125
137
|
*/
|
|
126
|
-
export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus = null } = {}) {
|
|
138
|
+
export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus = null, gate = null } = {}) {
|
|
127
139
|
const present = new Set(); // "a\0b" for every edge already known
|
|
128
140
|
const succ = new Map(); // a -> Set(b): the live successor relation
|
|
129
141
|
for (const [a, b] of edges || []) {
|
|
@@ -150,6 +162,7 @@ export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus =
|
|
|
150
162
|
const key = `${a}${SEP}${c}`;
|
|
151
163
|
if (present.has(key) || derivedKeys.has(key)) continue; // dedup / novelty screen
|
|
152
164
|
if (!inFocus(a, b, c)) continue; // focus-connection screen
|
|
165
|
+
if (gate?.declines(a, c)) continue; // sense screen
|
|
153
166
|
additions.push([a, b, c, key]);
|
|
154
167
|
}
|
|
155
168
|
}
|
|
@@ -183,7 +196,7 @@ export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus =
|
|
|
183
196
|
* `allEdges` is already closed (a prior pass ran to fixpoint), the output
|
|
184
197
|
* equals the full kernel's novel output, order included.
|
|
185
198
|
*/
|
|
186
|
-
export function deriveSubClassClosureDelta(allEdges, deltaEdges, { depth = 32, budget = 50, focus = null } = {}) {
|
|
199
|
+
export function deriveSubClassClosureDelta(allEdges, deltaEdges, { depth = 32, budget = 50, focus = null, gate = null } = {}) {
|
|
187
200
|
const present = new Set(); // "a\0b" for every edge already known
|
|
188
201
|
const succ = new Map(); // a -> Set(b): the live successor relation
|
|
189
202
|
const pred = new Map(); // b -> Set(a): its inverse, for the R∘Δ join
|
|
@@ -216,6 +229,7 @@ export function deriveSubClassClosureDelta(allEdges, deltaEdges, { depth = 32, b
|
|
|
216
229
|
const key = `${a}${SEP}${c}`;
|
|
217
230
|
if (present.has(key) || derivedKeys.has(key)) return; // dedup / novelty screen
|
|
218
231
|
if (!inFocus(a, b, c)) return; // focus-connection screen
|
|
232
|
+
if (gate?.declines(a, c)) return; // sense screen
|
|
219
233
|
additions.push([a, b, c, key]);
|
|
220
234
|
};
|
|
221
235
|
for (const [a, b] of delta) {
|
|
@@ -336,8 +350,13 @@ export function buildRelevanceFrontier(rows, seedTerms) {
|
|
|
336
350
|
* a delta caller that pre-filters `typeEdges` to the relevant slice passes the
|
|
337
351
|
* FULL list here, so an already-stored conclusion outside the slice is still
|
|
338
352
|
* recognized as known rather than re-derived.
|
|
353
|
+
*
|
|
354
|
+
* `gate` is the same SENSE screen `deriveSubClassClosure` takes, read over the
|
|
355
|
+
* individual and the class it would inherit: an entity placed under one top
|
|
356
|
+
* never picks up a type from a disjoint one because a band flattened two
|
|
357
|
+
* senses onto a class somewhere in its chain.
|
|
339
358
|
*/
|
|
340
|
-
export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, focus = null, presentTypeEdges = typeEdges } = {}) {
|
|
359
|
+
export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, focus = null, presentTypeEdges = typeEdges, gate = null } = {}) {
|
|
341
360
|
const ancestorsOf = buildAncestorCloser(subClassEdges);
|
|
342
361
|
|
|
343
362
|
const present = new Set(); // "x\0C" for every rdf:type edge already known
|
|
@@ -357,6 +376,7 @@ export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, f
|
|
|
357
376
|
const key = `${x}${SEP}${d}`;
|
|
358
377
|
if (present.has(key)) continue; // dedup / novelty screen
|
|
359
378
|
if (!inFocus(x, c, d)) continue; // focus-connection screen
|
|
379
|
+
if (gate?.declines(x, d)) continue; // sense screen
|
|
360
380
|
candidates.push([x, c, d, key]);
|
|
361
381
|
}
|
|
362
382
|
}
|
|
@@ -820,7 +840,9 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
|
|
|
820
840
|
* even when none of its own three terms was named), `maxEnvironments`
|
|
821
841
|
* (per-fact environment cap,
|
|
822
842
|
* default DEFAULT_MAX_ENVIRONMENTS), `full` (force full evaluation even with
|
|
823
|
-
* a valid watermark), `
|
|
843
|
+
* a valid watermark), `senseGate` (default true: screen scm-sco and cax-sco
|
|
844
|
+
* conclusions through sense-gate.mjs so a two-sense band word can't licence a
|
|
845
|
+
* walk across the join; false runs the kernels raw), `store` (REQUIRED — the memory store's
|
|
824
846
|
* { loadMemory, readFactRows, appendFacts } read/write functions, injected so
|
|
825
847
|
* this inference module never imports the store itself; optional
|
|
826
848
|
* loadSyllogiseState/saveSyllogiseState enable delta mode).
|
|
@@ -830,7 +852,7 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
|
|
|
830
852
|
*/
|
|
831
853
|
export async function syllogise(repoDir, {
|
|
832
854
|
depth = 32, budget = 50, focus = null, expandFocus = false,
|
|
833
|
-
maxEnvironments = DEFAULT_MAX_ENVIRONMENTS, full = false, store,
|
|
855
|
+
maxEnvironments = DEFAULT_MAX_ENVIRONMENTS, full = false, store, senseGate = true,
|
|
834
856
|
} = {}) {
|
|
835
857
|
const { loadMemory, readFactRows, appendFacts } = requireStore(store, ["loadMemory", "readFactRows", "appendFacts"], "syllogise");
|
|
836
858
|
const stateFnsPresent = typeof store?.loadSyllogiseState === "function" && typeof store?.saveSyllogiseState === "function";
|
|
@@ -857,6 +879,15 @@ export async function syllogise(repoDir, {
|
|
|
857
879
|
const target = someValuesFromOf.get(restriction);
|
|
858
880
|
if (target) restrictionEdges.push({ restriction, property, target });
|
|
859
881
|
}
|
|
882
|
+
// The sense gate reads STATED isa rows only. Feeding it a previous pass's
|
|
883
|
+
// own conclusions would let one crossing licence the next, which is the
|
|
884
|
+
// failure it exists to stop.
|
|
885
|
+
const gate = senseGate
|
|
886
|
+
? buildSenseGate({
|
|
887
|
+
subClassEdges: rows.filter((r) => isSubClassOf(r.predicate) && !isPurelyEntailed(r.provenance)).map((r) => [r.subject, r.object]),
|
|
888
|
+
typeEdges: rows.filter((r) => isType(r.predicate) && !isPurelyEntailed(r.provenance)).map((r) => [r.subject, r.object]),
|
|
889
|
+
})
|
|
890
|
+
: null;
|
|
860
891
|
const callerFocus = normalizeFocus(focus);
|
|
861
892
|
// An expanded focus is the same relevance walk a delta pass runs, seeded by
|
|
862
893
|
// the caller's terms instead of a change set — still a focus (never reads
|
|
@@ -898,8 +929,8 @@ export async function syllogise(repoDir, {
|
|
|
898
929
|
? deltaRows.filter((r) => isSubClassOf(r.predicate)).map((r) => [r.subject, r.object])
|
|
899
930
|
: [];
|
|
900
931
|
const scmDerived = mode === "delta"
|
|
901
|
-
? (deltaSubEdges.length ? deriveSubClassClosureDelta(subClassEdges, deltaSubEdges, { depth, budget, focus: normalizedFocus }) : [])
|
|
902
|
-
: deriveSubClassClosure(subClassEdges, { depth, budget, focus: normalizedFocus });
|
|
932
|
+
? (deltaSubEdges.length ? deriveSubClassClosureDelta(subClassEdges, deltaSubEdges, { depth, budget, focus: normalizedFocus, gate }) : [])
|
|
933
|
+
: deriveSubClassClosure(subClassEdges, { depth, budget, focus: normalizedFocus, gate });
|
|
903
934
|
// cax-sco sees the ENLARGED subClassOf edge set (stated ∪ this pass's own
|
|
904
935
|
// scm-sco conclusions) so both rules complete in one `tmct syllogise` call.
|
|
905
936
|
const enlargedSubClassEdges = subClassEdges.concat(scmDerived.map((d) => [d.subject, d.object]));
|
|
@@ -954,7 +985,7 @@ export async function syllogise(repoDir, {
|
|
|
954
985
|
|
|
955
986
|
const remainingBudget = Math.max(0, budget - scmDerived.length);
|
|
956
987
|
const caxDerived = remainingBudget > 0 && !deltaEmpty
|
|
957
|
-
? deriveTypePropagation(caxTypeEdges, enlargedSubClassEdges, { budget: remainingBudget, focus: kernelFocus, presentTypeEdges: typeEdges })
|
|
988
|
+
? deriveTypePropagation(caxTypeEdges, enlargedSubClassEdges, { budget: remainingBudget, focus: kernelFocus, presentTypeEdges: typeEdges, gate })
|
|
958
989
|
: [];
|
|
959
990
|
// cax-dw sees the SAME enlarged subClassOf set (so its own ⊑-lift reaches a
|
|
960
991
|
// chain scm-sco just grew this pass) — it doesn't need the enlarged TYPE
|
|
@@ -65,8 +65,23 @@ const NOISE_TERM_SETS = [
|
|
|
65
65
|
FOREIGN_PARTICLE_TERMS,
|
|
66
66
|
];
|
|
67
67
|
|
|
68
|
+
/** A version or model string shatters into pieces that are mostly digits and
|
|
69
|
+
* punctuation — "Qwen3.8-2.4T" leaves "8-2" and "4t" behind. A term with no
|
|
70
|
+
* letter at all, or one carrying a digit whose digits and punctuation match or
|
|
71
|
+
* outnumber its letters, states a quantity or a version; it never names a
|
|
72
|
+
* subject a reference lookup could define. A letter-only term with interior
|
|
73
|
+
* stops ("u.s.") carries no digit, so it stays. */
|
|
74
|
+
function isShapeNoiseTerm(term) {
|
|
75
|
+
const letters = (term.match(/[a-z]/g) || []).length;
|
|
76
|
+
if (!letters) return true;
|
|
77
|
+
const digits = (term.match(/\d/g) || []).length;
|
|
78
|
+
if (!digits) return false;
|
|
79
|
+
const punctuation = (term.match(/[^a-z0-9\s]/g) || []).length;
|
|
80
|
+
return digits + punctuation >= letters;
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
function isNoiseTerm(term) {
|
|
69
|
-
return NOISE_TERM_SETS.some((set) => set.has(term));
|
|
84
|
+
return NOISE_TERM_SETS.some((set) => set.has(term)) || isShapeNoiseTerm(term);
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
/** Ledger entry field order fixed once here so `ledgerPayload` serializes
|
package/src/services/chat.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import { uuidv7 } from "../adapters/uuid.mjs";
|
|
|
37
37
|
import * as defaultSource from "../adapters/source.mjs";
|
|
38
38
|
import { loadTemplates, render as renderTemplate } from "../adapters/corpus/templates.mjs";
|
|
39
39
|
import { rankByBiasThenTrust } from "../domain/memory/bias.mjs";
|
|
40
|
-
import { HAS_A_PREDICATE, loadMemory as loadMemoryStore, normFactPredicate, normFactTerm as normFactTermStatic, readFactRows as readStoredFactRows, readRuleRows as readStoredRuleRows } from "../adapters/memory/core.mjs";
|
|
40
|
+
import { HAS_A_PREDICATE, foldedFactRows as foldStoreFactRows, loadMemory as loadMemoryStore, normFactPredicate, normFactTerm as normFactTermStatic, readFactRows as readStoredFactRows, readRuleRows as readStoredRuleRows } from "../adapters/memory/core.mjs";
|
|
41
41
|
import { BACKEND_REJECTED_CODE, BACKEND_UNAVAILABLE_CODE } from "../adapters/memory/row-backend.mjs";
|
|
42
42
|
import {
|
|
43
43
|
CAPABILITY_REPORT_CAP, NEG_CAPABLE_OF_PREDICATE, NEG_SUBCLASS_PREDICATE, capabilityBaseRate,
|
|
@@ -8461,8 +8461,9 @@ async function memoryFacts(memoryDir) {
|
|
|
8461
8461
|
*
|
|
8462
8462
|
* `cache`: an optional, caller-owned plain object (`{ rows: null }`, e.g. one
|
|
8463
8463
|
* runTurn call's own `factRowsCache`) — when `cache.rows` is already
|
|
8464
|
-
* populated, it's returned directly, skipping
|
|
8465
|
-
*
|
|
8464
|
+
* populated, it's returned directly, skipping the store read entirely;
|
|
8465
|
+
* otherwise the fold is taken (from the store's own held one when it has a
|
|
8466
|
+
* current one — see core's `foldedFactRows`) and stashed onto
|
|
8466
8467
|
* `cache.rows` for the next caller sharing the same cache this turn.
|
|
8467
8468
|
* Absent/null (the default) reproduces a fresh, uncached reload every call,
|
|
8468
8469
|
* so every caller that doesn't pass one is byte-for-byte unaffected. Never
|
|
@@ -8473,8 +8474,8 @@ async function memoryFacts(memoryDir) {
|
|
|
8473
8474
|
async function factRows(memoryDir, cache = null) {
|
|
8474
8475
|
if (cache?.rows) return cache.rows;
|
|
8475
8476
|
try {
|
|
8476
|
-
const {
|
|
8477
|
-
const rows =
|
|
8477
|
+
const { foldedFactRows } = await import("../adapters/memory/core.mjs");
|
|
8478
|
+
const rows = await foldedFactRows(memoryDir);
|
|
8478
8479
|
if (cache) { cache.rows = rows; cache.reloads = (cache.reloads || 0) + 1; }
|
|
8479
8480
|
return rows;
|
|
8480
8481
|
} catch {
|
|
@@ -18476,15 +18477,19 @@ function finishTurn(result, ctx) {
|
|
|
18476
18477
|
return finish(result, ctx);
|
|
18477
18478
|
}
|
|
18478
18479
|
|
|
18479
|
-
/**
|
|
18480
|
-
*
|
|
18481
|
-
*
|
|
18482
|
-
*
|
|
18483
|
-
*
|
|
18484
|
-
* the
|
|
18480
|
+
/** A readFactRows() snapshot of the memory store — one half of the before/after
|
|
18481
|
+
* pair a turn's `factsTouched` diff is taken over. Deliberately bypasses the
|
|
18482
|
+
* turn's factRowsCache: the cache is invalidated by only some write paths, and
|
|
18483
|
+
* a diff read through it would miss the very writes it exists to report. The
|
|
18484
|
+
* store's own held fold is a different thing and safe to read: every write
|
|
18485
|
+
* moves the stamp it is keyed to, so a snapshot taken after a write is that
|
|
18486
|
+
* write's own fold, and a turn that wrote nothing gets back the array it
|
|
18487
|
+
* started from — which diffs to nothing, correctly. Null (rather than []) when
|
|
18488
|
+
* there is no store or it won't load, so the caller can tell "nothing to diff"
|
|
18489
|
+
* from "diffed, nothing moved". */
|
|
18485
18490
|
async function factRowSnapshot(memoryDir) {
|
|
18486
18491
|
if (!memoryDir) return null;
|
|
18487
|
-
try { return
|
|
18492
|
+
try { return await foldStoreFactRows(memoryDir); } catch { return null; }
|
|
18488
18493
|
}
|
|
18489
18494
|
|
|
18490
18495
|
/** The Fact rows this turn wrote, diffed against the snapshot taken before it,
|