@polycode-projects/the-mechanical-code-talker 6.0.18 → 6.0.20
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 +20 -23
- package/bin/tmct.mjs +16 -33
- package/corpus/LICENSES.json +0 -21
- package/corpus/README.md +10 -13
- package/corpus/reference/manifest.json +19 -19
- package/corpus/reference/shards/ref-01.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-04.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-08.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-10.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-11.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-17.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-20.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-25.jsonl.gz +0 -0
- package/corpus/reference/shards/ref-2c.jsonl.gz +0 -0
- package/corpus/tier2/generate.mjs +6 -142
- package/corpus/tier2/manifest.json +0 -42
- package/package.json +6 -4
- package/src/adapters/corpus/child-seed.mjs +74 -0
- package/src/adapters/corpus/conceptnet.mjs +45 -26
- package/src/adapters/corpus/research-source.mjs +6 -2
- package/src/adapters/corpus/wikidata-live.mjs +92 -51
- package/src/adapters/memory/blocks.mjs +7 -1
- package/src/adapters/memory/core.mjs +505 -107
- package/src/adapters/memory/corpus-bands.mjs +27 -10
- package/src/adapters/memory/inspect.mjs +24 -5
- package/src/adapters/memory/rows.mjs +359 -30
- package/src/adapters/memory/shacl.mjs +10 -3
- package/src/domain/ask.mjs +27 -10
- package/src/domain/cli-verbs.mjs +3 -4
- package/src/domain/completions/group.mjs +8 -3
- package/src/domain/completions/infer.mjs +7 -2
- package/src/domain/completions/prune.mjs +5 -1
- package/src/domain/completions/rank.mjs +7 -2
- package/src/domain/digest/compose.mjs +5 -1
- package/src/domain/digest/select.mjs +12 -6
- package/src/domain/domain.mjs +15 -8
- package/src/domain/el-classify.mjs +11 -2
- package/src/domain/fact-phrase.mjs +86 -4
- package/src/domain/hash.mjs +9 -0
- package/src/domain/memory/bias.mjs +8 -4
- package/src/domain/memory/capability.mjs +12 -6
- package/src/domain/memory/fact-order.mjs +29 -0
- package/src/domain/memory/resolution.mjs +3 -0
- package/src/domain/news-feed.mjs +862 -92
- package/src/domain/reference-pack.mjs +5 -0
- package/src/domain/sense-gate.mjs +220 -0
- package/src/domain/sense-scope.mjs +116 -0
- package/src/domain/sense-split.mjs +1 -1
- package/src/domain/syllogise.mjs +60 -21
- package/src/domain/tableau.mjs +23 -14
- package/src/domain/term-ledger.mjs +16 -1
- package/src/domain/worlds-pack.mjs +5 -1
- package/src/services/adventure-autoplay.mjs +6 -1
- package/src/services/adventure-editor.mjs +43 -21
- package/src/services/adventure-viz.mjs +26 -9
- package/src/services/adventure.mjs +40 -10
- package/src/services/chat.mjs +270 -125
- package/src/services/extensions.mjs +51 -58
- package/src/services/extract-facts.mjs +906 -66
- package/src/services/init.mjs +4 -4
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +4 -5
- package/src/services/mud-editor.mjs +40 -16
- package/src/services/mud-viz.mjs +8 -2
- package/src/services/mudiii-turn.mjs +5 -3
- package/src/services/mudiii-viz.mjs +8 -2
- package/src/services/news.mjs +306 -21
- package/src/services/research-viz.mjs +1 -1
- package/src/services/sprite-catalog-viz.mjs +10 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +6 -12
- package/src/surfaces/web/memory-ask-browser.bundle.js +152 -151
- package/src/surfaces/web/mud-browser-entry.mjs +7 -11
- package/src/surfaces/web/research-browser-entry.mjs +5 -2
- package/corpus/tier2/aws.jsonl +0 -39
- package/corpus/tier2/java.jsonl +0 -31
- package/corpus/tier2/python.jsonl +0 -30
|
@@ -226,9 +226,14 @@ function withoutParentheticals(text) {
|
|
|
226
226
|
|
|
227
227
|
// Words that end the noun phrase: a relative clause or a trailing modifier
|
|
228
228
|
// carries detail, not the category ("a place where ships shelter" → place).
|
|
229
|
+
// "about"/"around"/"roughly"/"approximately" open a comparison or a topic
|
|
230
|
+
// that reads like more of the head phrase but is not ("a small marsupial
|
|
231
|
+
// about the size of a large cat" is a marsupial, not a size; "a book about
|
|
232
|
+
// dogs" is a book).
|
|
229
233
|
const ISA_CLAUSE_CUT = new Set([
|
|
230
234
|
"that", "which", "where", "who", "whose", "whom", "when", "used", "found",
|
|
231
235
|
"made", "with", "in", "on", "for", "from", "by", "to", "and", "or",
|
|
236
|
+
"about", "around", "roughly", "approximately",
|
|
232
237
|
]);
|
|
233
238
|
// Classifier heads carry no category of their own ("a member of the cat
|
|
234
239
|
// family" names family membership, not what the thing is) — no isa beats a
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// sense-scope.mjs — the same-sense discipline a READ-TIME walk applies when it
|
|
2
|
+
// picks which neighbours of a term to show.
|
|
3
|
+
//
|
|
4
|
+
// sense-gate.mjs already places a term under its nearest top classes and says
|
|
5
|
+
// when two terms are provably in different senses. It uses that to refuse a
|
|
6
|
+
// DERIVED isa edge, and it refuses only on proof: both ends placed, every cross
|
|
7
|
+
// pair declared disjoint. That burden is right for a derivation, because
|
|
8
|
+
// refusing one deletes an entailment the graph would otherwise hold.
|
|
9
|
+
//
|
|
10
|
+
// Selection carries the opposite burden. Nothing is deleted when a background
|
|
11
|
+
// row goes unshown, so a walk may ask for evidence that a neighbour belongs
|
|
12
|
+
// before it spends a line on it. This module asks for exactly that: the
|
|
13
|
+
// neighbour's top classes must MEET the anchor's, not merely fail to be
|
|
14
|
+
// provably disjoint.
|
|
15
|
+
//
|
|
16
|
+
// The difference is the whole bug. "russia" places under `place`; "passage"
|
|
17
|
+
// places under `artifact`. Those two tops overlap by declaration (a cathedral
|
|
18
|
+
// is both), so the derivation gate lets them stand and the store holds an
|
|
19
|
+
// entailed "russia is a kind of passage". A hop-bounded walk out of russia then
|
|
20
|
+
// treats `passage` as a one-hop neighbour and comes back down the anatomy side
|
|
21
|
+
// of it: "orifice is a kind of passage", "duct is a kind of passage", on a card
|
|
22
|
+
// about a prisoner release. Requiring a shared top stops the walk at `passage`
|
|
23
|
+
// while `country`, `district` and `geographical area` — all of which meet
|
|
24
|
+
// `place` — carry straight on.
|
|
25
|
+
//
|
|
26
|
+
// Unplaced is not out of sense. A term the bands never classify (a person's
|
|
27
|
+
// name, a coined product name, most of what a headline is actually about) has
|
|
28
|
+
// no tops to meet, so it is admitted.
|
|
29
|
+
//
|
|
30
|
+
// An unplaced ANCHOR is the harder case: it pools no tops, so by default the
|
|
31
|
+
// scope keeps to nothing and every neighbour walks in as filler. Two ways out,
|
|
32
|
+
// and a caller picks between them. `hasPlacedSense` says whether an anchor set
|
|
33
|
+
// is placed at all, so a caller holding a second, better placed set can anchor
|
|
34
|
+
// there instead. `admitAllWhenUnplaced: false` says these anchors ARE the
|
|
35
|
+
// sense, placed or not: an unplaced anchor set then keeps to the unplaced, so a
|
|
36
|
+
// walk under it goes sparse instead of pulling in the whole placed graph. A
|
|
37
|
+
// graph with no bands at all places nothing, refuses nothing, and reads exactly
|
|
38
|
+
// as it did.
|
|
39
|
+
//
|
|
40
|
+
// Pure over the fact set. The gate underneath sorts every frontier and memoizes
|
|
41
|
+
// per term, and an anchor set is a membership test over the tops it pools, so
|
|
42
|
+
// two ingestion orders of the same facts admit the same terms.
|
|
43
|
+
|
|
44
|
+
import { normFactTerm } from "./hash.mjs";
|
|
45
|
+
import { buildSenseGate } from "./sense-gate.mjs";
|
|
46
|
+
|
|
47
|
+
const SUBCLASS_PREDICATE = "rdfs:subClassOf";
|
|
48
|
+
const TYPE_PREDICATE = "rdf:type";
|
|
49
|
+
|
|
50
|
+
/** Whether a row states its isa edge rather than concluding it. An entailment
|
|
51
|
+
* head, a hypothetical's environment, or a justification chain all mark the
|
|
52
|
+
* graph's own output; feeding that back to the gate that constrains it is how
|
|
53
|
+
* a bad shortcut ends up vouching for itself. Read here rather than imported,
|
|
54
|
+
* so this module stands alone for any reader that wants the same discipline. */
|
|
55
|
+
function statesItsIsaEdge(row) {
|
|
56
|
+
const head = String(row?.provenance || "").trim().split(/\s+/)[0] || "";
|
|
57
|
+
if (head.startsWith("entailed:")) return false;
|
|
58
|
+
if (Array.isArray(row?.environments) && row.environments.length) return false;
|
|
59
|
+
if (Array.isArray(row?.justification) && row.justification.length) return false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Builds the scope over one fact set, reading only the asserted isa rows.
|
|
64
|
+
*
|
|
65
|
+
* Returns `{ topsOf, hasPlacedSense, sameSenseAs }`. `topsOf(term)` is the
|
|
66
|
+
* gate's own placement, exposed so a caller can explain a refusal.
|
|
67
|
+
* `sameSenseAs(anchors)` takes one term or an iterable of them and answers a
|
|
68
|
+
* `(term) => boolean` predicate: true when the term may stay in the anchors'
|
|
69
|
+
* neighbourhood. Several anchors pool their tops, so a walk seeded from more
|
|
70
|
+
* than one term keeps to the senses of all of them. `hasPlacedSense(anchors)`
|
|
71
|
+
* says whether that pool holds anything, which is whether the scope those
|
|
72
|
+
* anchors build refuses any term at all. Pass `admitAllWhenUnplaced: false`
|
|
73
|
+
* and an unplaced anchor set refuses every placed term instead of admitting
|
|
74
|
+
* everything. */
|
|
75
|
+
export function buildSenseScope(rows) {
|
|
76
|
+
const subClassEdges = [];
|
|
77
|
+
const typeEdges = [];
|
|
78
|
+
for (const row of rows || []) {
|
|
79
|
+
if (!row || !statesItsIsaEdge(row)) continue;
|
|
80
|
+
const child = normFactTerm(row.subject);
|
|
81
|
+
const parent = normFactTerm(row.object);
|
|
82
|
+
if (!child || !parent) continue;
|
|
83
|
+
if (row.predicate === SUBCLASS_PREDICATE) subClassEdges.push([child, parent]);
|
|
84
|
+
else if (row.predicate === TYPE_PREDICATE) typeEdges.push([child, parent]);
|
|
85
|
+
}
|
|
86
|
+
const gate = buildSenseGate({ subClassEdges, typeEdges });
|
|
87
|
+
|
|
88
|
+
const topsOf = (term) => gate.topsOf(normFactTerm(term));
|
|
89
|
+
|
|
90
|
+
const anchorList = (anchors) => (typeof anchors === "string" ? [anchors] : [...(anchors || [])]);
|
|
91
|
+
|
|
92
|
+
function topsAcross(list) {
|
|
93
|
+
const tops = new Set();
|
|
94
|
+
for (const anchor of list) for (const top of topsOf(anchor)) tops.add(top);
|
|
95
|
+
return tops;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const hasPlacedSense = (anchors) => topsAcross(anchorList(anchors)).size > 0;
|
|
99
|
+
|
|
100
|
+
function sameSenseAs(anchors, { admitAllWhenUnplaced = true } = {}) {
|
|
101
|
+
const list = anchorList(anchors);
|
|
102
|
+
const anchorTops = topsAcross(list);
|
|
103
|
+
if (!anchorTops.size && admitAllWhenUnplaced) return () => true;
|
|
104
|
+
const anchorTerms = new Set(list.map((a) => normFactTerm(a)).filter(Boolean));
|
|
105
|
+
return (term) => {
|
|
106
|
+
const norm = normFactTerm(term);
|
|
107
|
+
if (!norm || anchorTerms.has(norm)) return true;
|
|
108
|
+
const tops = topsOf(norm);
|
|
109
|
+
if (!tops.length) return true;
|
|
110
|
+
for (const top of tops) if (anchorTops.has(top)) return true;
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { topsOf, hasPlacedSense, sameSenseAs };
|
|
116
|
+
}
|
|
@@ -260,7 +260,7 @@ export function clusterSenses(objects, {
|
|
|
260
260
|
let best = depth(label);
|
|
261
261
|
for (const m of sorted) { const d = depth(m); if (d > best) { best = d; label = m; } }
|
|
262
262
|
return { objects: sorted, label };
|
|
263
|
-
}).sort((x, y) => x.label
|
|
263
|
+
}).sort((x, y) => compareStrings(x.label, y.label));
|
|
264
264
|
|
|
265
265
|
return { split: clusters.length > 1, clusters, pairs };
|
|
266
266
|
}
|
package/src/domain/syllogise.mjs
CHANGED
|
@@ -20,11 +20,25 @@
|
|
|
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";
|
|
34
|
+
|
|
35
|
+
/** Codepoint order, never localeCompare. Every candidate list below is sorted
|
|
36
|
+
* and then cut at the budget, so the comparator decides WHICH entailments a
|
|
37
|
+
* pass derives. It also decides which premise each one cites: dedup keeps the
|
|
38
|
+
* first candidate per key, so a derivation's `via` slot holds whichever middle
|
|
39
|
+
* term sorted first. Sorting either by the reader's OS locale would let two
|
|
40
|
+
* machines forward-chain one graph into two different fact sets. */
|
|
41
|
+
const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
28
42
|
|
|
29
43
|
/** The two persisting entry points (syllogise, retractSubClassOf) take the
|
|
30
44
|
* memory store's read/write functions through a required `store` option —
|
|
@@ -122,8 +136,14 @@ function normalizeFocus(focus) {
|
|
|
122
136
|
* (not already present), each `{ subject, object, via }`, bounded by `budget`
|
|
123
137
|
* and `depth`, focus-filtered, tautology- and dedup-screened, in a deterministic
|
|
124
138
|
* order. No I/O — this is the whole inference kernel, unit-testable in isolation.
|
|
139
|
+
*
|
|
140
|
+
* `gate` (sense-gate.mjs's `buildSenseGate`, or null) is the SENSE screen: a
|
|
141
|
+
* candidate a⊑c is dropped when the gate places a and c under top classes the
|
|
142
|
+
* ontology declares disjoint, so a band that flattens two word senses onto one
|
|
143
|
+
* label cannot license a walk across the join. Null (the default) leaves the
|
|
144
|
+
* kernel's behaviour exactly as it was.
|
|
125
145
|
*/
|
|
126
|
-
export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus = null } = {}) {
|
|
146
|
+
export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus = null, gate = null } = {}) {
|
|
127
147
|
const present = new Set(); // "a\0b" for every edge already known
|
|
128
148
|
const succ = new Map(); // a -> Set(b): the live successor relation
|
|
129
149
|
for (const [a, b] of edges || []) {
|
|
@@ -150,12 +170,13 @@ export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus =
|
|
|
150
170
|
const key = `${a}${SEP}${c}`;
|
|
151
171
|
if (present.has(key) || derivedKeys.has(key)) continue; // dedup / novelty screen
|
|
152
172
|
if (!inFocus(a, b, c)) continue; // focus-connection screen
|
|
173
|
+
if (gate?.declines(a, c)) continue; // sense screen
|
|
153
174
|
additions.push([a, b, c, key]);
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
178
|
if (!additions.length) break; // fixpoint reached
|
|
158
|
-
additions.sort((x, y) => x[0]
|
|
179
|
+
additions.sort((x, y) => byCodepoint(x[0], y[0]) || byCodepoint(x[2], y[2]) || byCodepoint(x[1], y[1]));
|
|
159
180
|
let progressed = false;
|
|
160
181
|
for (const [a, b, c, key] of additions) {
|
|
161
182
|
if (derivedKeys.has(key)) continue; // an earlier addition this round covered it
|
|
@@ -183,7 +204,7 @@ export function deriveSubClassClosure(edges, { depth = 32, budget = 50, focus =
|
|
|
183
204
|
* `allEdges` is already closed (a prior pass ran to fixpoint), the output
|
|
184
205
|
* equals the full kernel's novel output, order included.
|
|
185
206
|
*/
|
|
186
|
-
export function deriveSubClassClosureDelta(allEdges, deltaEdges, { depth = 32, budget = 50, focus = null } = {}) {
|
|
207
|
+
export function deriveSubClassClosureDelta(allEdges, deltaEdges, { depth = 32, budget = 50, focus = null, gate = null } = {}) {
|
|
187
208
|
const present = new Set(); // "a\0b" for every edge already known
|
|
188
209
|
const succ = new Map(); // a -> Set(b): the live successor relation
|
|
189
210
|
const pred = new Map(); // b -> Set(a): its inverse, for the R∘Δ join
|
|
@@ -216,6 +237,7 @@ export function deriveSubClassClosureDelta(allEdges, deltaEdges, { depth = 32, b
|
|
|
216
237
|
const key = `${a}${SEP}${c}`;
|
|
217
238
|
if (present.has(key) || derivedKeys.has(key)) return; // dedup / novelty screen
|
|
218
239
|
if (!inFocus(a, b, c)) return; // focus-connection screen
|
|
240
|
+
if (gate?.declines(a, c)) return; // sense screen
|
|
219
241
|
additions.push([a, b, c, key]);
|
|
220
242
|
};
|
|
221
243
|
for (const [a, b] of delta) {
|
|
@@ -223,7 +245,7 @@ export function deriveSubClassClosureDelta(allEdges, deltaEdges, { depth = 32, b
|
|
|
223
245
|
for (const z of pred.get(a) || []) consider(z, a, b); // R∘Δ
|
|
224
246
|
}
|
|
225
247
|
if (!additions.length) break; // fixpoint reached
|
|
226
|
-
additions.sort((x, y) => x[0]
|
|
248
|
+
additions.sort((x, y) => byCodepoint(x[0], y[0]) || byCodepoint(x[2], y[2]) || byCodepoint(x[1], y[1]));
|
|
227
249
|
let progressed = false;
|
|
228
250
|
const nextDelta = [];
|
|
229
251
|
for (const [a, b, c, key] of additions) {
|
|
@@ -336,8 +358,13 @@ export function buildRelevanceFrontier(rows, seedTerms) {
|
|
|
336
358
|
* a delta caller that pre-filters `typeEdges` to the relevant slice passes the
|
|
337
359
|
* FULL list here, so an already-stored conclusion outside the slice is still
|
|
338
360
|
* recognized as known rather than re-derived.
|
|
361
|
+
*
|
|
362
|
+
* `gate` is the same SENSE screen `deriveSubClassClosure` takes, read over the
|
|
363
|
+
* individual and the class it would inherit: an entity placed under one top
|
|
364
|
+
* never picks up a type from a disjoint one because a band flattened two
|
|
365
|
+
* senses onto a class somewhere in its chain.
|
|
339
366
|
*/
|
|
340
|
-
export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, focus = null, presentTypeEdges = typeEdges } = {}) {
|
|
367
|
+
export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, focus = null, presentTypeEdges = typeEdges, gate = null } = {}) {
|
|
341
368
|
const ancestorsOf = buildAncestorCloser(subClassEdges);
|
|
342
369
|
|
|
343
370
|
const present = new Set(); // "x\0C" for every rdf:type edge already known
|
|
@@ -357,10 +384,11 @@ export function deriveTypePropagation(typeEdges, subClassEdges, { budget = 50, f
|
|
|
357
384
|
const key = `${x}${SEP}${d}`;
|
|
358
385
|
if (present.has(key)) continue; // dedup / novelty screen
|
|
359
386
|
if (!inFocus(x, c, d)) continue; // focus-connection screen
|
|
387
|
+
if (gate?.declines(x, d)) continue; // sense screen
|
|
360
388
|
candidates.push([x, c, d, key]);
|
|
361
389
|
}
|
|
362
390
|
}
|
|
363
|
-
candidates.sort((p, q) => p[0]
|
|
391
|
+
candidates.sort((p, q) => byCodepoint(p[0], q[0]) || byCodepoint(p[2], q[2]) || byCodepoint(p[1], q[1]));
|
|
364
392
|
const derived = [];
|
|
365
393
|
const derivedKeys = new Set();
|
|
366
394
|
for (const [x, c, d, key] of candidates) {
|
|
@@ -426,7 +454,7 @@ export function deriveDisjointViolations(typeEdges, subClassEdges, disjointEdges
|
|
|
426
454
|
}
|
|
427
455
|
}
|
|
428
456
|
}
|
|
429
|
-
candidates.sort((p, q) => p[0]
|
|
457
|
+
candidates.sort((p, q) => byCodepoint(p[0], q[0]) || byCodepoint(p[3], q[3]) || byCodepoint(p[2], q[2]) || byCodepoint(p[1], q[1]));
|
|
430
458
|
const derived = [];
|
|
431
459
|
const derivedKeys = new Set();
|
|
432
460
|
for (const [x, c, d, e, key] of candidates) {
|
|
@@ -506,7 +534,7 @@ export function deriveSomeValuesFromApplication(propertyEdges, typeEdges, subCla
|
|
|
506
534
|
}
|
|
507
535
|
}
|
|
508
536
|
}
|
|
509
|
-
candidates.sort((a, b) => a[0]
|
|
537
|
+
candidates.sort((a, b) => byCodepoint(a[0], b[0]) || byCodepoint(a[6], b[6]) || byCodepoint(a[1], b[1]) || byCodepoint(a[3], b[3]));
|
|
510
538
|
const derived = [];
|
|
511
539
|
const derivedKeys = new Set();
|
|
512
540
|
for (const [x, p, pKey, y, c, target, r, key] of candidates) {
|
|
@@ -555,7 +583,7 @@ export function buildCardinalityRestrictions(rows) {
|
|
|
555
583
|
if (!onClass) continue;
|
|
556
584
|
restrictions.push({ restriction, kind, n, onClass });
|
|
557
585
|
}
|
|
558
|
-
restrictions.sort((a, b) => a.restriction
|
|
586
|
+
restrictions.sort((a, b) => byCodepoint(a.restriction, b.restriction));
|
|
559
587
|
return restrictions;
|
|
560
588
|
}
|
|
561
589
|
|
|
@@ -607,7 +635,7 @@ export function deriveSomeValuesFromSubsumption(restrictionEdges, subClassEdges,
|
|
|
607
635
|
}
|
|
608
636
|
}
|
|
609
637
|
}
|
|
610
|
-
candidates.sort((a, b) => a[0]
|
|
638
|
+
candidates.sort((a, b) => byCodepoint(a[0], b[0]) || byCodepoint(a[1], b[1]));
|
|
611
639
|
const derived = [];
|
|
612
640
|
const derivedKeys = new Set();
|
|
613
641
|
for (const [c1, c2, y1, y2, key] of candidates) {
|
|
@@ -775,7 +803,7 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
|
|
|
775
803
|
}
|
|
776
804
|
}
|
|
777
805
|
}
|
|
778
|
-
candidates.sort((p, q) => p[0]
|
|
806
|
+
candidates.sort((p, q) => byCodepoint(p[0], q[0]) || byCodepoint(p[1], q[1]) || byCodepoint(p[2], q[2]));
|
|
779
807
|
const derived = [];
|
|
780
808
|
for (const [x, ta, tb, viaA, viaB] of candidates) {
|
|
781
809
|
if (derived.length >= budget) break;
|
|
@@ -820,7 +848,9 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
|
|
|
820
848
|
* even when none of its own three terms was named), `maxEnvironments`
|
|
821
849
|
* (per-fact environment cap,
|
|
822
850
|
* default DEFAULT_MAX_ENVIRONMENTS), `full` (force full evaluation even with
|
|
823
|
-
* a valid watermark), `
|
|
851
|
+
* a valid watermark), `senseGate` (default true: screen scm-sco and cax-sco
|
|
852
|
+
* conclusions through sense-gate.mjs so a two-sense band word can't licence a
|
|
853
|
+
* walk across the join; false runs the kernels raw), `store` (REQUIRED — the memory store's
|
|
824
854
|
* { loadMemory, readFactRows, appendFacts } read/write functions, injected so
|
|
825
855
|
* this inference module never imports the store itself; optional
|
|
826
856
|
* loadSyllogiseState/saveSyllogiseState enable delta mode).
|
|
@@ -830,7 +860,7 @@ export function findConsistencyViolations(typeEdges, subClassEdges, disjointEdge
|
|
|
830
860
|
*/
|
|
831
861
|
export async function syllogise(repoDir, {
|
|
832
862
|
depth = 32, budget = 50, focus = null, expandFocus = false,
|
|
833
|
-
maxEnvironments = DEFAULT_MAX_ENVIRONMENTS, full = false, store,
|
|
863
|
+
maxEnvironments = DEFAULT_MAX_ENVIRONMENTS, full = false, store, senseGate = true,
|
|
834
864
|
} = {}) {
|
|
835
865
|
const { loadMemory, readFactRows, appendFacts } = requireStore(store, ["loadMemory", "readFactRows", "appendFacts"], "syllogise");
|
|
836
866
|
const stateFnsPresent = typeof store?.loadSyllogiseState === "function" && typeof store?.saveSyllogiseState === "function";
|
|
@@ -857,6 +887,15 @@ export async function syllogise(repoDir, {
|
|
|
857
887
|
const target = someValuesFromOf.get(restriction);
|
|
858
888
|
if (target) restrictionEdges.push({ restriction, property, target });
|
|
859
889
|
}
|
|
890
|
+
// The sense gate reads STATED isa rows only. Feeding it a previous pass's
|
|
891
|
+
// own conclusions would let one crossing licence the next, which is the
|
|
892
|
+
// failure it exists to stop.
|
|
893
|
+
const gate = senseGate
|
|
894
|
+
? buildSenseGate({
|
|
895
|
+
subClassEdges: rows.filter((r) => isSubClassOf(r.predicate) && !isPurelyEntailed(r.provenance)).map((r) => [r.subject, r.object]),
|
|
896
|
+
typeEdges: rows.filter((r) => isType(r.predicate) && !isPurelyEntailed(r.provenance)).map((r) => [r.subject, r.object]),
|
|
897
|
+
})
|
|
898
|
+
: null;
|
|
860
899
|
const callerFocus = normalizeFocus(focus);
|
|
861
900
|
// An expanded focus is the same relevance walk a delta pass runs, seeded by
|
|
862
901
|
// the caller's terms instead of a change set — still a focus (never reads
|
|
@@ -898,8 +937,8 @@ export async function syllogise(repoDir, {
|
|
|
898
937
|
? deltaRows.filter((r) => isSubClassOf(r.predicate)).map((r) => [r.subject, r.object])
|
|
899
938
|
: [];
|
|
900
939
|
const scmDerived = mode === "delta"
|
|
901
|
-
? (deltaSubEdges.length ? deriveSubClassClosureDelta(subClassEdges, deltaSubEdges, { depth, budget, focus: normalizedFocus }) : [])
|
|
902
|
-
: deriveSubClassClosure(subClassEdges, { depth, budget, focus: normalizedFocus });
|
|
940
|
+
? (deltaSubEdges.length ? deriveSubClassClosureDelta(subClassEdges, deltaSubEdges, { depth, budget, focus: normalizedFocus, gate }) : [])
|
|
941
|
+
: deriveSubClassClosure(subClassEdges, { depth, budget, focus: normalizedFocus, gate });
|
|
903
942
|
// cax-sco sees the ENLARGED subClassOf edge set (stated ∪ this pass's own
|
|
904
943
|
// scm-sco conclusions) so both rules complete in one `tmct syllogise` call.
|
|
905
944
|
const enlargedSubClassEdges = subClassEdges.concat(scmDerived.map((d) => [d.subject, d.object]));
|
|
@@ -954,7 +993,7 @@ export async function syllogise(repoDir, {
|
|
|
954
993
|
|
|
955
994
|
const remainingBudget = Math.max(0, budget - scmDerived.length);
|
|
956
995
|
const caxDerived = remainingBudget > 0 && !deltaEmpty
|
|
957
|
-
? deriveTypePropagation(caxTypeEdges, enlargedSubClassEdges, { budget: remainingBudget, focus: kernelFocus, presentTypeEdges: typeEdges })
|
|
996
|
+
? deriveTypePropagation(caxTypeEdges, enlargedSubClassEdges, { budget: remainingBudget, focus: kernelFocus, presentTypeEdges: typeEdges, gate })
|
|
958
997
|
: [];
|
|
959
998
|
// cax-dw sees the SAME enlarged subClassOf set (so its own ⊑-lift reaches a
|
|
960
999
|
// chain scm-sco just grew this pass) — it doesn't need the enlarged TYPE
|
|
@@ -1115,7 +1154,7 @@ export async function syllogise(repoDir, {
|
|
|
1115
1154
|
environments: environmentsOf(r), provenance: r.provenance,
|
|
1116
1155
|
}));
|
|
1117
1156
|
const alternateCandidates = [...conclusionCandidates, ...storedCandidates]
|
|
1118
|
-
.sort((a, b) => a.subject
|
|
1157
|
+
.sort((a, b) => byCodepoint(a.subject, b.subject) || byCodepoint(a.predicate, b.predicate) || byCodepoint(a.object, b.object));
|
|
1119
1158
|
let environmentsAdded = 0;
|
|
1120
1159
|
let alternatesTruncated = false;
|
|
1121
1160
|
let examined = 0;
|
|
@@ -1399,7 +1438,7 @@ function buildSupportEnumerator(rows) {
|
|
|
1399
1438
|
if (rec) {
|
|
1400
1439
|
const edges = [...(propertyEdgesOf.get(row.subject) || [])]
|
|
1401
1440
|
.filter(([p]) => normFactTerm(p) === rec.propertyKey)
|
|
1402
|
-
.sort((a, b) => a[0]
|
|
1441
|
+
.sort((a, b) => byCodepoint(a[0], b[0]) || byCodepoint(a[1], b[1]));
|
|
1403
1442
|
for (const [p, y] of edges) {
|
|
1404
1443
|
if (out.length >= maxEnvironments) break;
|
|
1405
1444
|
for (const c of [...(typesOf.get(y) || [])].sort()) {
|
|
@@ -1426,7 +1465,7 @@ function buildSupportEnumerator(rows) {
|
|
|
1426
1465
|
if ((disjointOf.get(d) || new Set()).has(row.object)) pairs.push([c, d]);
|
|
1427
1466
|
}
|
|
1428
1467
|
}
|
|
1429
|
-
pairs.sort((a, b) => a[0]
|
|
1468
|
+
pairs.sort((a, b) => byCodepoint(a[0], b[0]) || byCodepoint(a[1], b[1]));
|
|
1430
1469
|
for (const [c, d] of pairs) {
|
|
1431
1470
|
if (out.length >= maxEnvironments) break;
|
|
1432
1471
|
const dwStoredForward = disjointForward.has(`${d}${SEP}${row.object}`);
|
|
@@ -1622,7 +1661,7 @@ export async function retractSubClassOf(repoDir, subject, object, {
|
|
|
1622
1661
|
}
|
|
1623
1662
|
}
|
|
1624
1663
|
const candidates = [...candidateIds].map((id) => byId.get(id))
|
|
1625
|
-
.sort((a, b) => a.subject
|
|
1664
|
+
.sort((a, b) => byCodepoint(a.subject, b.subject) || byCodepoint(a.predicate, b.predicate) || byCodepoint(a.object, b.object));
|
|
1626
1665
|
if (!candidates.length) break; // fixpoint — nothing cites what just fell
|
|
1627
1666
|
|
|
1628
1667
|
// The surviving fact set for THIS round excludes every candidate's own
|
|
@@ -1688,7 +1727,7 @@ export async function retractSubClassOf(repoDir, subject, object, {
|
|
|
1688
1727
|
if (appendFactsFn) {
|
|
1689
1728
|
const regroundWrites = [...reground.entries()]
|
|
1690
1729
|
.filter(([id]) => !removed.has(id))
|
|
1691
|
-
.sort((a, b) => a[0]
|
|
1730
|
+
.sort((a, b) => byCodepoint(a[0], b[0]))
|
|
1692
1731
|
.map(([id, environments]) => {
|
|
1693
1732
|
const row = byId.get(id);
|
|
1694
1733
|
return {
|