@hviana/sema 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -7
- package/dist/src/alu/src/index.d.ts +1 -1
- package/dist/src/alu/src/index.js +1 -1
- package/dist/src/alu/src/parser.js +2 -6
- package/dist/src/alu/src/resonance.d.ts +13 -0
- package/dist/src/alu/src/resonance.js +41 -0
- package/dist/src/alu/test/alu.test.js +39 -0
- package/dist/src/bytes.d.ts +6 -2
- package/dist/src/bytes.js +10 -4
- package/dist/src/canon.js +44 -0
- package/dist/src/geometry.d.ts +19 -1
- package/dist/src/geometry.js +125 -141
- package/dist/src/meter.d.ts +20 -0
- package/dist/src/meter.js +21 -1
- package/dist/src/mind/articulation.js +14 -1
- package/dist/src/mind/attention.d.ts +12 -0
- package/dist/src/mind/attention.js +44 -16
- package/dist/src/mind/bridge.js +3 -3
- package/dist/src/mind/derivation.d.ts +40 -0
- package/dist/src/mind/derivation.js +34 -0
- package/dist/src/mind/graph-search.d.ts +89 -15
- package/dist/src/mind/graph-search.js +345 -174
- package/dist/src/mind/learning.js +1 -1
- package/dist/src/mind/mechanisms/cover.d.ts +19 -3
- package/dist/src/mind/mechanisms/cover.js +101 -58
- package/dist/src/mind/mechanisms/recall.js +0 -1
- package/dist/src/mind/mind.js +2 -2
- package/dist/src/mind/pipeline.d.ts +5 -1
- package/dist/src/mind/pipeline.js +175 -87
- package/dist/src/mind/primitives.d.ts +25 -5
- package/dist/src/mind/primitives.js +107 -44
- package/dist/src/mind/reasoning.d.ts +18 -4
- package/dist/src/mind/reasoning.js +445 -321
- package/dist/src/mind/recognition.js +29 -13
- package/dist/src/mind/resonance.js +1 -11
- package/dist/src/mind/traverse.d.ts +3 -3
- package/dist/src/mind/traverse.js +3 -3
- package/dist/src/mind/types.d.ts +7 -1
- package/dist/src/store-sqlite.d.ts +25 -0
- package/dist/src/store-sqlite.js +89 -1
- package/dist/src/store.d.ts +48 -4
- package/dist/src/store.js +86 -6
- package/docs/INDEX.md +18 -18
- package/docs/INVARIANTS.md +16 -16
- package/docs/architecture/bounded-reads.md +1 -1
- package/docs/architecture/caches.md +5 -4
- package/docs/architecture/closure.md +45 -5
- package/docs/architecture/cost-model.md +16 -0
- package/docs/architecture/factored-machinery.md +14 -13
- package/docs/architecture/fold-contract.md +51 -1
- package/docs/architecture/mechanism-market.md +21 -0
- package/docs/architecture/memoization.md +3 -3
- package/docs/architecture/meter.md +2 -1
- package/docs/architecture/saturation.md +12 -0
- package/docs/architecture/store.md +25 -2
- package/docs/failures/tempting-but-wrong.md +13 -2
- package/docs/harness/gates.md +12 -10
- package/docs/mechanisms/cover.md +23 -6
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alu/README.md +10 -2
- package/src/alu/src/index.ts +1 -0
- package/src/alu/src/parser.ts +6 -6
- package/src/alu/src/resonance.ts +42 -0
- package/src/alu/test/alu.test.ts +40 -0
- package/src/bytes.ts +13 -3
- package/src/canon.ts +40 -0
- package/src/geometry.ts +183 -154
- package/src/meter.ts +21 -1
- package/src/mind/articulation.ts +14 -2
- package/src/mind/attention.ts +47 -25
- package/src/mind/bridge.ts +3 -3
- package/src/mind/derivation.ts +77 -0
- package/src/mind/graph-search.ts +449 -221
- package/src/mind/learning.ts +1 -7
- package/src/mind/match.ts +1 -2
- package/src/mind/mechanisms/cast.ts +1 -2
- package/src/mind/mechanisms/cover.ts +149 -84
- package/src/mind/mechanisms/extraction.ts +1 -2
- package/src/mind/mechanisms/prefix-completion.ts +1 -1
- package/src/mind/mechanisms/recall.ts +1 -3
- package/src/mind/mechanisms/reference.ts +1 -1
- package/src/mind/mind.ts +5 -30
- package/src/mind/pipeline.ts +206 -102
- package/src/mind/primitives.ts +119 -43
- package/src/mind/reasoning.ts +558 -413
- package/src/mind/recognition.ts +24 -9
- package/src/mind/resonance.ts +2 -16
- package/src/mind/trace.ts +1 -1
- package/src/mind/traverse.ts +3 -3
- package/src/mind/types.ts +9 -11
- package/src/store-sqlite.ts +92 -1
- package/src/store.ts +113 -7
- package/test/105-derive-through-reports-its-refusal.test.mjs +8 -5
- package/test/106-the-join-fires.test.mjs +21 -0
- package/test/111-the-cover-assembly-is-counted.test.mjs +8 -5
- package/test/128-the-leads-somewhere-pair-agrees.test.mjs +18 -12
- package/test/136-the-two-named-limits.test.mjs +3 -2
- package/test/137-the-law-lives-once-and-below.test.mjs +21 -0
- package/test/148-exact-shortcuts-agree.test.mjs +188 -0
- package/test/149-the-closure-engine.test.mjs +138 -0
- package/test/150-the-join-is-output-sensitive.test.mjs +66 -0
- package/test/151-the-cover-pays-for-what-it-reaches.test.mjs +142 -0
- package/test/152-the-read-side-names-as-the-write-side.test.mjs +146 -0
- package/test/153-a-cheaper-bound-is-looked-at-first.test.mjs +155 -0
- package/test/24-generalization.test.mjs +32 -0
- package/test/36-bloom.test.mjs +53 -0
- package/test/37-cluster-dispersion-fusion.test.mjs +75 -0
- package/test/48-recognise-turn-connective.test.mjs +3 -2
- package/test/55-cost-meter.test.mjs +4 -4
- package/test/90-connector-read-cap.test.mjs +7 -7
|
@@ -48,9 +48,6 @@ function pushInto(mp, k, v) {
|
|
|
48
48
|
else
|
|
49
49
|
mp.set(k, [v]);
|
|
50
50
|
}
|
|
51
|
-
/** Read the chosen spans back off a derivation: the goal is a chain of bridge
|
|
52
|
-
* steps, each whose second premise is the `out` it crossed. Walk the chain to
|
|
53
|
-
* the axiom and reverse into left-to-right order. */
|
|
54
51
|
/** The BYTE TERM of a derivation's cost: how many bytes its bridge rule charged
|
|
55
52
|
* at PASS each — read back off the rule that charged them (`bridgeRule`:
|
|
56
53
|
* `o.rec ? MICRO : PASS * (o.j - o.i)`), so the split exists in ONE place and
|
|
@@ -74,6 +71,9 @@ function readBridgedBytes(derivation) {
|
|
|
74
71
|
}
|
|
75
72
|
return bytes;
|
|
76
73
|
}
|
|
74
|
+
/** Read the chosen spans back off a derivation: the goal is a chain of bridge
|
|
75
|
+
* steps, each whose second premise is the `out` it crossed. Walk the chain to
|
|
76
|
+
* the axiom and reverse into left-to-right order. */
|
|
77
77
|
function readCover(derivation) {
|
|
78
78
|
const segs = [];
|
|
79
79
|
let node = derivation;
|
|
@@ -95,6 +95,43 @@ function readCover(derivation) {
|
|
|
95
95
|
segs.reverse();
|
|
96
96
|
return segs;
|
|
97
97
|
}
|
|
98
|
+
/** The chart's canonical key — an item's boundary signature (see
|
|
99
|
+
* {@link DeductionSystem.key}). Module-level because the join license
|
|
100
|
+
* ({@link GraphSearch.solve}) names facts by the same key the chart does. */
|
|
101
|
+
function chartKey(it) {
|
|
102
|
+
if (it.kind === "cover")
|
|
103
|
+
return "c" + it.p;
|
|
104
|
+
if (it.kind === "form") {
|
|
105
|
+
return `f${it.i}.${it.j}.${it.node}.${it.via ? 1 : 0}.${it.rcmp ? 1 : 0}${it.ask ? "?" : ""}`;
|
|
106
|
+
}
|
|
107
|
+
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${it.fix ? 1 : 0}.${it.throughFact ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
108
|
+
}
|
|
109
|
+
/** The facts a derivation STANDS ON — every node-bearing `out` in its proof
|
|
110
|
+
* tree, premises before conclusions, each once. */
|
|
111
|
+
function factsOf(root) {
|
|
112
|
+
const out = [];
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
const walk = (d) => {
|
|
115
|
+
if (seen.has(d))
|
|
116
|
+
return;
|
|
117
|
+
seen.add(d);
|
|
118
|
+
for (const p of d.premises)
|
|
119
|
+
walk(p);
|
|
120
|
+
if (d.item.kind === "out" && d.item.node !== undefined)
|
|
121
|
+
out.push(d.item);
|
|
122
|
+
};
|
|
123
|
+
walk(root);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** Whether a run asked for anything — its cover is then provisional. */
|
|
127
|
+
function asked(concepts, connectors) {
|
|
128
|
+
return concepts.asked.size > 0 || (connectors?.asked.size ?? 0) > 0;
|
|
129
|
+
}
|
|
130
|
+
/** Nothing offered: a cover with no async premises (a nested completion,
|
|
131
|
+
* articulation's revoicing). */
|
|
132
|
+
export function noLicence() {
|
|
133
|
+
return { offered: new Set(), granted: new Map(), asked: new Set() };
|
|
134
|
+
}
|
|
98
135
|
/** Flatten a {@link GItem} into the {@link DerivationItem} a rationale shows. */
|
|
99
136
|
function derivationItem(it) {
|
|
100
137
|
if (it.kind === "cover")
|
|
@@ -217,17 +254,9 @@ export class GraphSearch {
|
|
|
217
254
|
this.maxGroup = maxGroup;
|
|
218
255
|
this.host = host;
|
|
219
256
|
}
|
|
220
|
-
/** The
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
* a join that filters the query's own subject by RAW bytes re-admits it —
|
|
224
|
-
* measured: that is the trap's wrong answer (`The capital of Eiffel Tower
|
|
225
|
-
* country is Berlin.`). Cached by query identity, because the search is
|
|
226
|
-
* reused across responses. */
|
|
227
|
-
/* * The hub bound √N (bounded-reads.md) — the ONE
|
|
228
|
-
* fan-out cap, stated here rather than imported from `traverse.ts` because
|
|
229
|
-
* this module is deliberately host-based (it holds a bare Store, never a
|
|
230
|
-
* MindContext).
|
|
257
|
+
/** The hub bound √N (bounded-reads.md) — the ONE fan-out cap, stated here
|
|
258
|
+
* rather than imported from `traverse.ts` because this module is
|
|
259
|
+
* deliberately host-based (it holds a bare Store, never a MindContext).
|
|
231
260
|
* That is the same write/read-side duplication convention canonical.ts's
|
|
232
261
|
* header documents: if the formula changes it must change in BOTH places.
|
|
233
262
|
* It is stated ONCE per side, though — the expression used to be spelled
|
|
@@ -251,8 +280,10 @@ export class GraphSearch {
|
|
|
251
280
|
*
|
|
252
281
|
* Any learnt connector between two rewrites is spliced IN by the in-search
|
|
253
282
|
* connector rule (see {@link outRules}), so the returned spans already carry
|
|
254
|
-
* it — there is no post-pass.
|
|
255
|
-
|
|
283
|
+
* it — there is no post-pass. With a {@link ConnectorLicence}, a cover
|
|
284
|
+
* returned while `connectors.asked` is non-empty is PROVISIONAL: grant the
|
|
285
|
+
* asked pairs and cover again. */
|
|
286
|
+
cover(queryLen, sites, concepts, leaves, splits, substitutions, connectors, computedResults,
|
|
256
287
|
/** When given, receives each solved span's lightest derivation — the full
|
|
257
288
|
* adapted A*LD proof tree as classified {@link DerivationStep}s — for the
|
|
258
289
|
* TOP cover AND every nested completion the sink is threaded into (see
|
|
@@ -265,11 +296,15 @@ export class GraphSearch {
|
|
|
265
296
|
// through (completion is cover, recursively — see {@link recompleteNode}).
|
|
266
297
|
this.recompleteOpen.clear();
|
|
267
298
|
this.recompleteMemo = new Map();
|
|
299
|
+
this.entityMemo = new Map();
|
|
268
300
|
// The top cover's derivation sink is threaded into every nested completion
|
|
269
301
|
// so the recompositions a produced form needs are reported in the same
|
|
270
302
|
// trace instead of vanishing after the first layer.
|
|
271
303
|
this.derivationSink = onDerivation;
|
|
272
|
-
|
|
304
|
+
concepts.asked.clear();
|
|
305
|
+
if (connectors)
|
|
306
|
+
connectors.asked.clear();
|
|
307
|
+
const solved = this.solve(queryLen, { sites, leaves, splits }, concepts, substitutions, connectors, computedResults, onDerivation);
|
|
273
308
|
// Deepening runs HERE, once, on the derivation the top cover CHOSE — never
|
|
274
309
|
// inside the nested solve a completion runs. Nesting the deepening is what
|
|
275
310
|
// made per-query cost track how densely the corpus interconnects the forms
|
|
@@ -277,7 +312,10 @@ export class GraphSearch {
|
|
|
277
312
|
// continuations instead of following the chain the answer itself licensed.
|
|
278
313
|
// With deepening only at the top, `recompleteNode` walks the accepted chain
|
|
279
314
|
// one link at a time (its own memo and stack), so the work is the answer's.
|
|
280
|
-
|
|
315
|
+
// A provisional cover is not deepened: it is about to be re-covered.
|
|
316
|
+
if (solved === null || asked(concepts, connectors))
|
|
317
|
+
return solved;
|
|
318
|
+
return {
|
|
281
319
|
segs: this.deepen(solved.segs),
|
|
282
320
|
cost: solved.cost,
|
|
283
321
|
moves: solved.moves,
|
|
@@ -296,18 +334,78 @@ export class GraphSearch {
|
|
|
296
334
|
* decomposition may therefore run as deep as the graph licenses — three
|
|
297
335
|
* decomposes, two recomposes, any mix — and stops only when it reaches a node
|
|
298
336
|
* that leads nowhere new, never at an arbitrary count. */
|
|
299
|
-
solve(spanLen, recognition,
|
|
300
|
-
|
|
337
|
+
solve(spanLen, recognition, concepts, substitutions, connectors, computedResults, onDerivation) {
|
|
338
|
+
// The query's own bytes, tiled from its perceived leaves. A JOIN reads the
|
|
339
|
+
// tail a produced fact's contained entity has to combine with, and the
|
|
340
|
+
// search otherwise only ever sees positions, never the bytes behind them.
|
|
341
|
+
const queryBytes = new Uint8Array(spanLen);
|
|
342
|
+
for (const lf of recognition.leaves) {
|
|
343
|
+
queryBytes.set(lf.bytes.subarray(0, lf.end - lf.start), lf.start);
|
|
344
|
+
}
|
|
345
|
+
// The nodes this span's recognition names — read ONCE per solve, on the first
|
|
346
|
+
// join that asks, instead of re-reading the memo per fact.
|
|
347
|
+
let queryNodeSet;
|
|
348
|
+
const queryNodes = () => queryNodeSet ??= new Set((this.host.recogniseSpan?.(queryBytes)?.sites ?? []).map((s) => s.payload));
|
|
349
|
+
// THE JOIN DERIVES THROUGH THE FACTS THE DERIVATION STANDS ON — never
|
|
350
|
+
// through every fact the exploration REACHES. How many facts the search
|
|
351
|
+
// reaches is set by how densely the corpus interconnects the forms on the
|
|
352
|
+
// way (a dialogue hub's continuations, walked to their fixpoints, are
|
|
353
|
+
// thousands), and pricing a join from each of them made the work track the
|
|
354
|
+
// corpus instead of the answer: measured on the 31.7M-node store, a 46-byte
|
|
355
|
+
// query reached 2,000+ fixpoints in a minute, each paying an entity scan,
|
|
356
|
+
// past 10 GB. It is the trap {@link recompleteNode} already records, and
|
|
357
|
+
// the same cure as {@link deepen}: the expensive step runs on what the
|
|
358
|
+
// lightest derivation CHOSE.
|
|
359
|
+
//
|
|
360
|
+
// So the search runs to its lightest derivation, the facts that derivation
|
|
361
|
+
// stands on are LICENSED — their joins computed once — and it runs again
|
|
362
|
+
// with those joins as ordinary rules of the same system, priced exactly as
|
|
363
|
+
// before (the fact's own cost + STEP). A join's conclusion is itself a fact
|
|
364
|
+
// the next derivation may stand on, so chains extend round by round until
|
|
365
|
+
// no newly licensed fact joins anything: the depth is the answer's, never a
|
|
366
|
+
// count. A round is re-run only when a newly licensed fact DOES join, so a
|
|
367
|
+
// query whose facts join nothing pays one search, as before.
|
|
368
|
+
//
|
|
369
|
+
// The joins a covering round licensed are KEPT across a licence's re-covers:
|
|
370
|
+
// a round that asked for nothing is the very round the next re-cover repeats
|
|
371
|
+
// (see {@link Licence}), so its joins are the ones that re-cover would grant
|
|
372
|
+
// again.
|
|
373
|
+
const joins = this.joinsKept.get(concepts) ?? new Map();
|
|
374
|
+
this.joinsKept.set(concepts, joins);
|
|
301
375
|
// Search-effort accounting (src/meter.ts): the chart's pops/pushes are
|
|
302
376
|
// the cover's real cost, and a heuristic that stops being admissible
|
|
303
377
|
// shows up as a pop count that explodes while the answer stays the same.
|
|
304
378
|
const meter = this.host.meter;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
meter
|
|
309
|
-
|
|
310
|
-
meter
|
|
379
|
+
let derivation;
|
|
380
|
+
for (;;) {
|
|
381
|
+
const system = this.buildSearch(spanLen, recognition.sites, concepts, recognition.leaves, recognition.splits, queryBytes, joins, substitutions, connectors, computedResults);
|
|
382
|
+
const stats = meter ? { pops: 0, pushes: 0 } : undefined;
|
|
383
|
+
derivation = lightestDerivation(system, stats);
|
|
384
|
+
if (meter && stats) {
|
|
385
|
+
meter.searches++;
|
|
386
|
+
meter.searchPops += stats.pops;
|
|
387
|
+
meter.searchPushes += stats.pushes;
|
|
388
|
+
}
|
|
389
|
+
// A round that ASKED for a connector is provisional: return it before any
|
|
390
|
+
// join is licensed from a derivation the granted connectors may change.
|
|
391
|
+
if (derivation === null || asked(concepts, connectors))
|
|
392
|
+
break;
|
|
393
|
+
let joined = false;
|
|
394
|
+
for (const fact of factsOf(derivation)) {
|
|
395
|
+
const k = chartKey(fact);
|
|
396
|
+
if (joins.has(k))
|
|
397
|
+
continue;
|
|
398
|
+
if (meter)
|
|
399
|
+
meter.joinFacts++;
|
|
400
|
+
const granted = [
|
|
401
|
+
...this.deriveThrough(fact, queryBytes, spanLen, queryNodes),
|
|
402
|
+
].map((r) => ({ conclusion: r.conclusion, cost: r.cost }));
|
|
403
|
+
joins.set(k, granted);
|
|
404
|
+
if (granted.length > 0)
|
|
405
|
+
joined = true;
|
|
406
|
+
}
|
|
407
|
+
if (!joined)
|
|
408
|
+
break;
|
|
311
409
|
}
|
|
312
410
|
// When covering under a substitution map (articulation), a form→out rule is
|
|
313
411
|
// the form EMITTING the asker's voice, not grounding to its own answer — so
|
|
@@ -354,20 +452,13 @@ export class GraphSearch {
|
|
|
354
452
|
* adjacent fragments toward a known leaf (findLeaf) or branch (findBranch);
|
|
355
453
|
* a completion fused with its neighbour may spell a deeper learned form the
|
|
356
454
|
* flat probes can't name, recovered canonically by {@link resolve}. */
|
|
357
|
-
buildSearch(queryLen, sites,
|
|
455
|
+
buildSearch(queryLen, sites, concepts, leaves, splits, queryBytes, joins, substitutions, connectors, computedResults) {
|
|
358
456
|
const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
|
|
359
457
|
// Same corpus-scale hub floor {@link atomIsHub}/{@link atomReach} (traverse.ts)
|
|
360
458
|
// derive for byte atoms — see {@link hubBound} below for why this module
|
|
361
459
|
// states the formula itself instead of importing it.
|
|
362
460
|
const atomsAreHubs = Math.max(1, Math.ceil((this.store.edgeSourceCount() * W) / 256)) > this.hubBound();
|
|
363
461
|
const nodeBytes = (n) => this.store.bytesPrefix(n, ALL);
|
|
364
|
-
// The query's own bytes, tiled from its perceived leaves. A JOIN reads the
|
|
365
|
-
// tail a produced fact's contained entity has to combine with, and `buildSearch`
|
|
366
|
-
// otherwise only ever sees positions, never the bytes behind them.
|
|
367
|
-
const queryBytes = new Uint8Array(queryLen);
|
|
368
|
-
for (const lf of leaves) {
|
|
369
|
-
queryBytes.set(lf.bytes.subarray(0, lf.end - lf.start), lf.start);
|
|
370
|
-
}
|
|
371
462
|
// Content-addressed probes over the store's hash-cons maps — the same keys
|
|
372
463
|
// training filled. No byte-by-byte trie walk.
|
|
373
464
|
const findLeafU = (b) => this.store.findLeaf(b) ?? undefined;
|
|
@@ -378,30 +469,80 @@ export class GraphSearch {
|
|
|
378
469
|
const outsByEnd = new Map();
|
|
379
470
|
const outsByNode = new Map();
|
|
380
471
|
const coverableByStart = new Map();
|
|
381
|
-
// Index the connectors by their left and right answer-node, so the
|
|
382
|
-
// rule iterates only this out's FEW
|
|
383
|
-
// N-ary case O(parts) keys) instead of scanning every position
|
|
384
|
-
// keeps the in-search bridge bounded when many parts are
|
|
472
|
+
// Index the offered connectors by their left and right answer-node, so the
|
|
473
|
+
// connector rule iterates only this out's FEW offered partners (selective,
|
|
474
|
+
// and for the N-ary case O(parts) keys) instead of scanning every position
|
|
475
|
+
// pair — what keeps the in-search bridge bounded when many parts are
|
|
476
|
+
// recognised at once.
|
|
385
477
|
const linksByLeft = new Map();
|
|
386
478
|
const linksByRight = new Map();
|
|
387
479
|
if (connectors) {
|
|
388
|
-
for (const
|
|
480
|
+
for (const key of connectors.offered) {
|
|
389
481
|
const comma = key.indexOf(",");
|
|
390
482
|
const l = Number(key.slice(0, comma));
|
|
391
483
|
const r = Number(key.slice(comma + 1));
|
|
392
|
-
pushInto(linksByLeft, l, [r,
|
|
393
|
-
pushInto(linksByRight, r, [l,
|
|
484
|
+
pushInto(linksByLeft, l, [r, key]);
|
|
485
|
+
pushInto(linksByRight, r, [l, key]);
|
|
394
486
|
}
|
|
395
487
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
488
|
+
// A SPAN'S CHEAPEST COMPLETION DOMINATES THE REST. Coverage is positional:
|
|
489
|
+
// every recognised completion of [i, j) advances the cover from i to j at
|
|
490
|
+
// the same MICRO, so for the goal only the cheapest matters. What else a
|
|
491
|
+
// completion can do turns on its BYTES — fuse it with a neighbour, splice a
|
|
492
|
+
// connector, join through it — and those rules fire from the completion the
|
|
493
|
+
// search would stand on for that span, never from every alternative it
|
|
494
|
+
// reached: the cure {@link deepen} and the join license already apply, for
|
|
495
|
+
// the same trap. Measured on the 31.7M-node store, a 46-byte dialogue query
|
|
496
|
+
// reached 60,530 continuation forms and 57,072 fixpoints for its few spans —
|
|
497
|
+
// a greeting's thousands of replies, each a fixpoint at the same depth — and
|
|
498
|
+
// canonically resolved every one of them fused with its neighbours (5.4 s
|
|
499
|
+
// of an 8.6 s cover) to stand, in the end, on two hops.
|
|
500
|
+
//
|
|
501
|
+
// So a form or completion of [i, j) whose cost has reached that of a
|
|
502
|
+
// completion of [i, j) already yielded is DOMINATED and fires nothing: every
|
|
503
|
+
// completion it could lead to costs at least as much (local costs are
|
|
504
|
+
// non-negative and a form's rules keep its span), and a tie goes to the one
|
|
505
|
+
// yielded first — the evidence-preferred continuation, yielded first by
|
|
506
|
+
// {@link formRules}. This is also what makes the stop-here offer a real
|
|
507
|
+
// horizon: a chain deeper than its first hop's stop (STEP + CONCEPT) is
|
|
508
|
+
// dominated by it, where before the PASS-priced goal let the search buy a
|
|
509
|
+
// thousand hops to look for one more byte.
|
|
510
|
+
const spanBest = new Map();
|
|
511
|
+
const spanOf = (i, j) => i * (queryLen + 1) + j;
|
|
512
|
+
const completes = (it) => it.kind === "out" && it.rec && it.cover;
|
|
513
|
+
const dominated = (it, cost) => {
|
|
514
|
+
if (it.kind === "cover")
|
|
515
|
+
return false;
|
|
516
|
+
if (it.kind === "out" && !completes(it))
|
|
517
|
+
return false;
|
|
518
|
+
const best = spanBest.get(spanOf(it.i, it.j));
|
|
519
|
+
if (best === undefined || best.cost > cost) {
|
|
520
|
+
if (completes(it))
|
|
521
|
+
spanBest.set(spanOf(it.i, it.j), { cost, item: it });
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
return best.item !== it &&
|
|
525
|
+
(it.kind !== "out" || chartKey(best.item) !== chartKey(it));
|
|
526
|
+
};
|
|
527
|
+
const completing = function* (rules, costOf) {
|
|
528
|
+
for (const r of rules) {
|
|
529
|
+
const c = r.conclusion;
|
|
530
|
+
if (completes(c)) {
|
|
531
|
+
let cost = r.cost;
|
|
532
|
+
for (const p of r.premises)
|
|
533
|
+
cost += costOf(p);
|
|
534
|
+
const s = spanOf(c.i, c.j);
|
|
535
|
+
const best = spanBest.get(s);
|
|
536
|
+
if (best === undefined || cost < best.cost) {
|
|
537
|
+
spanBest.set(s, { cost, item: c });
|
|
538
|
+
}
|
|
402
539
|
}
|
|
403
|
-
|
|
404
|
-
}
|
|
540
|
+
yield r;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const meter = this.host.meter;
|
|
544
|
+
return {
|
|
545
|
+
key: chartKey,
|
|
405
546
|
*axioms() {
|
|
406
547
|
yield { item: { kind: "cover", p: 0 }, cost: 0 };
|
|
407
548
|
// One out per tree leaf — content-defined chunks, far fewer than bytes.
|
|
@@ -487,14 +628,19 @@ export class GraphSearch {
|
|
|
487
628
|
const right = it.kind === "cover" ? it.p : it.j;
|
|
488
629
|
return (queryLen - right) * MICRO;
|
|
489
630
|
},
|
|
490
|
-
rules: (it) => {
|
|
631
|
+
rules: (it, costOf) => {
|
|
491
632
|
if (it.kind === "cover") {
|
|
492
633
|
return this.coverRules(it, coversDone, coverableByStart);
|
|
493
634
|
}
|
|
635
|
+
if (dominated(it, costOf(it))) {
|
|
636
|
+
if (meter)
|
|
637
|
+
meter.searchDominated++;
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
494
640
|
if (it.kind === "form") {
|
|
495
|
-
return this.formRules(it,
|
|
641
|
+
return completing(this.formRules(it, concepts, substitutions, nodeBytes), costOf);
|
|
496
642
|
}
|
|
497
|
-
return this.outRules(it, {
|
|
643
|
+
return completing(this.outRules(it, {
|
|
498
644
|
W,
|
|
499
645
|
splits,
|
|
500
646
|
atomsAreHubs,
|
|
@@ -507,9 +653,9 @@ export class GraphSearch {
|
|
|
507
653
|
findBranchU,
|
|
508
654
|
linksByLeft,
|
|
509
655
|
linksByRight,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
});
|
|
656
|
+
connectors,
|
|
657
|
+
joins,
|
|
658
|
+
}), costOf);
|
|
513
659
|
},
|
|
514
660
|
};
|
|
515
661
|
}
|
|
@@ -551,16 +697,22 @@ export class GraphSearch {
|
|
|
551
697
|
}
|
|
552
698
|
/** The connector-SPLICE rule for an oriented (l, r) pair, or null when the
|
|
553
699
|
* pair does not qualify — the ONE body behind {@link outRules}' two
|
|
554
|
-
* mirror loops (this-as-left over
|
|
555
|
-
* over
|
|
700
|
+
* mirror loops (this-as-left over offered right partners, this-as-right
|
|
701
|
+
* over offered left partners). Fires only when both sides are
|
|
556
702
|
* recognised, r starts at or after l ends, and the gap between them is
|
|
557
703
|
* empty or wholly recognised — never across the asker's own literal
|
|
558
|
-
* separator.
|
|
559
|
-
|
|
704
|
+
* separator — and the pair's connector is granted and learnt. A qualifying
|
|
705
|
+
* pair not yet granted is ASKED for (see {@link ConnectorLicence}). */
|
|
706
|
+
trySplice(l, r, key, licence, outsByEnd) {
|
|
560
707
|
if (!l.rec || !r.rec || r.i < l.j)
|
|
561
708
|
return null;
|
|
562
709
|
if (!this.gapRecognised(l.j, r.i, outsByEnd))
|
|
563
710
|
return null;
|
|
711
|
+
const link = licence.granted.get(key);
|
|
712
|
+
if (link === undefined)
|
|
713
|
+
licence.asked.add(key);
|
|
714
|
+
if (!link)
|
|
715
|
+
return null;
|
|
564
716
|
return {
|
|
565
717
|
premises: [l, r],
|
|
566
718
|
conclusion: {
|
|
@@ -577,7 +729,12 @@ export class GraphSearch {
|
|
|
577
729
|
}
|
|
578
730
|
/** form(i,j,node,via): follow the graph out of `node`, or (in articulation)
|
|
579
731
|
* emit its substitute voice directly. */
|
|
580
|
-
*formRules(it,
|
|
732
|
+
*formRules(it, concepts, substitutions, nodeBytes) {
|
|
733
|
+
// An asking form reached the agenda's front: the hop it stands for is due.
|
|
734
|
+
if (it.ask) {
|
|
735
|
+
concepts.asked.add(it.node);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
581
738
|
// Articulation: emit voice bytes at the recognised span; the hop/concept/
|
|
582
739
|
// emit chain is suppressed — the form contributes only its substitute.
|
|
583
740
|
if (substitutions) {
|
|
@@ -611,41 +768,17 @@ export class GraphSearch {
|
|
|
611
768
|
// guard then dead-ends it) with no way to reach the forward edge.
|
|
612
769
|
// Forking offers every continuation as its own rule so the one that
|
|
613
770
|
// genuinely advances (not a duplicate) is still reachable.
|
|
614
|
-
// A CHAIN HOP OFFERS ONLY WHAT THE QUESTION CAN PAY FOR.
|
|
615
771
|
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// continuations than the question has units —
|
|
627
|
-
// `ceil(queryLen / W)`, floored at 2 for plurality. It is QUERY-sized
|
|
628
|
-
// (invariant 5: no per-query read grows with N) and it leaves `hubBound`
|
|
629
|
-
// and every read untouched.
|
|
630
|
-
// THE OFFER IS THE CORPUS'S OWN STRUCTURE, and the search pays for
|
|
631
|
-
// exploring it. There is no offer cap here any more: the traversal cap I
|
|
632
|
-
// had put on this hop was a short-circuit — it bounded what a hop could
|
|
633
|
-
// OFFER instead of charging for it — and it was not needed.
|
|
634
|
-
//
|
|
635
|
-
// MEASURED in the regime where it used to bite (`hubBound = ceil(√N)`
|
|
636
|
-
// GREATER than the hub's degree — reached in a fixture by choosing the
|
|
637
|
-
// degree below √N, so the trained store is not needed): with the cap the
|
|
638
|
-
// offer was 8/9/9 continuations at degrees 35/70/120; without it, 52/84/120
|
|
639
|
-
// — and the WORK is LINEAR in the degree, not quadratic: pushes 262/296/332,
|
|
640
|
-
// perceptions 530/592/757, while the PEAK is identical with and without the
|
|
641
|
-
// cap (218/415/689 MB against 215/410/662) because it is set by the store,
|
|
642
|
-
// not by the fan-out. What made this hop expensive was never the fan-out
|
|
643
|
-
// breadth: it was the per-offer work, two duplicate/oversized computations
|
|
644
|
-
// since removed (the per-offset canonical scan, and the tail scan now
|
|
645
|
-
// restricted to the fold's boundaries).
|
|
646
|
-
//
|
|
647
|
-
// The residual, stated: the trained store's hub (degree 1 083) is an
|
|
648
|
-
// EXTRAPOLATION from this linear shape, not a measurement.
|
|
772
|
+
// THE OFFER IS THE CORPUS'S OWN STRUCTURE, and the search pays for it.
|
|
773
|
+
// In the closure law's terms (derivation.ts) a hop is a MOVE — it reaches
|
|
774
|
+
// structure the derivation has not stood on — so it is admitted on that
|
|
775
|
+
// ground and priced one STEP; the A*LD search, not a count, decides how
|
|
776
|
+
// many are worth taking. `hubBound` is the READ cap (invariant 5) and
|
|
777
|
+
// nothing else bounds the offer: a cap on what a hop may OFFER was tried
|
|
778
|
+
// and removed as a short-circuit (trap 13). Measured with the hub's
|
|
779
|
+
// degree below √N: the work is LINEAR in the degree (pushes 262/296/332 at
|
|
780
|
+
// degrees 35/70/120) and the peak is set by the store, not the fan-out
|
|
781
|
+
// (test/112 pins the non-quadratic shape).
|
|
649
782
|
const nx = this.store.nextFirst(it.node, this.hubBound());
|
|
650
783
|
// Count what is OFFERED, not what was read: the evidence-preferred
|
|
651
784
|
// continuation is yielded too, even when it lies outside the cap.
|
|
@@ -799,8 +932,26 @@ export class GraphSearch {
|
|
|
799
932
|
// Recognised but edge-less: borrow a concept (halo) sibling's edge. No
|
|
800
933
|
// edge and no concept means the form leads nowhere — it yields no rule, so
|
|
801
934
|
// a query of only such forms produces no derivation, and think is silent.
|
|
802
|
-
|
|
803
|
-
|
|
935
|
+
// An offered form not yet granted yields the hop's ASKING form at the
|
|
936
|
+
// hop's own cost and span (see {@link Licence}).
|
|
937
|
+
if (!concepts.offered.has(it.node))
|
|
938
|
+
return;
|
|
939
|
+
const target = concepts.granted.get(it.node);
|
|
940
|
+
if (target === undefined) {
|
|
941
|
+
yield {
|
|
942
|
+
premises: [it],
|
|
943
|
+
conclusion: {
|
|
944
|
+
kind: "form",
|
|
945
|
+
i: it.i,
|
|
946
|
+
j: it.j,
|
|
947
|
+
node: it.node,
|
|
948
|
+
via: true,
|
|
949
|
+
ask: true,
|
|
950
|
+
},
|
|
951
|
+
cost: CONCEPT,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
else if (target !== null) {
|
|
804
955
|
yield {
|
|
805
956
|
premises: [it],
|
|
806
957
|
conclusion: {
|
|
@@ -937,7 +1088,7 @@ export class GraphSearch {
|
|
|
937
1088
|
sites: [...recognised, ...structural],
|
|
938
1089
|
leaves: rec.leaves,
|
|
939
1090
|
splits: rec.splits,
|
|
940
|
-
},
|
|
1091
|
+
}, noLicence(), undefined, undefined, undefined, this.derivationSink);
|
|
941
1092
|
const answer = solved && concatBytes(solved.segs.map((s) => s.bytes));
|
|
942
1093
|
// ACCEPT, then CONTINUE THE CHAIN — but only along an accepted
|
|
943
1094
|
// completion. A re-cover whose result is not itself a learnt node (the
|
|
@@ -966,6 +1117,14 @@ export class GraphSearch {
|
|
|
966
1117
|
* outs of a long query re-cover each distinct node at most once); reset at the
|
|
967
1118
|
* top of {@link cover}. */
|
|
968
1119
|
recompleteMemo = new Map();
|
|
1120
|
+
/** {@link entityProposals}, per fact BYTES — a pure function of them while
|
|
1121
|
+
* the store is read-only (one response), so a fact the chart reaches as
|
|
1122
|
+
* several items (cover/fix variants, nested completions) is scanned once.
|
|
1123
|
+
* Reset at the top of {@link cover}, like {@link recompleteMemo}. */
|
|
1124
|
+
entityMemo = new Map();
|
|
1125
|
+
/** The joins licensed under one cover's {@link Licence}, kept across its
|
|
1126
|
+
* re-covers (see {@link solve}). */
|
|
1127
|
+
joinsKept = new WeakMap();
|
|
969
1128
|
/** The derivation sink of the TOP cover, threaded into every nested
|
|
970
1129
|
* completion so a produced form's own recompositions are reported in the
|
|
971
1130
|
* same trace instead of vanishing after the first layer. Undefined when
|
|
@@ -980,6 +1139,57 @@ export class GraphSearch {
|
|
|
980
1139
|
* most once per cover. A Set, not a flag, because it states WHICH node is
|
|
981
1140
|
* open — the invariant a reader needs to check the guard. */
|
|
982
1141
|
recompleteOpen = new Set();
|
|
1142
|
+
/** The ENTITIES a produced fact's bytes contain that lead somewhere — the
|
|
1143
|
+
* candidates a join may travel through. The forms the fact CONTAINS, by the
|
|
1144
|
+
* same recogniser the query went through (so the evidence standard is the
|
|
1145
|
+
* query's), plus — because the recognition of a STORED WHOLE returns the
|
|
1146
|
+
* whole and stops (measured: one site, the fact's own node, for `The
|
|
1147
|
+
* director of Eva is Gustaf Molander.`) — the longest canonically-resolving
|
|
1148
|
+
* form at each offset. A byte atom is never a subject. The scan runs only
|
|
1149
|
+
* for a FORM (≥ W: per letter it measured 20-26 s in test/99), and each probe
|
|
1150
|
+
* is a `canonResolve` whose exact tier decides a miss by its segment probe,
|
|
1151
|
+
* without a fold. The admission predicate is the store's `leadsSomewhere`
|
|
1152
|
+
* (edge or halo); the host lends its memoised form when it can.
|
|
1153
|
+
*
|
|
1154
|
+
* The SOURCE of each proposal travels with it, so a refusal names which path
|
|
1155
|
+
* proposed the candidate. Memoised per fact bytes ({@link entityMemo}). */
|
|
1156
|
+
entityProposals(factBytes) {
|
|
1157
|
+
const memoKey = latin1(factBytes);
|
|
1158
|
+
const hit = this.entityMemo.get(memoKey);
|
|
1159
|
+
if (hit !== undefined)
|
|
1160
|
+
return hit;
|
|
1161
|
+
const leads = (id) => this.host.leadsSomewhere !== undefined
|
|
1162
|
+
? this.host.leadsSomewhere(id)
|
|
1163
|
+
: this.store.leadsSomewhere(id);
|
|
1164
|
+
const sites = this.host.recogniseSpan?.(factBytes)?.sites ?? [];
|
|
1165
|
+
const proposed = new Map();
|
|
1166
|
+
const source = new Map();
|
|
1167
|
+
for (const s of sites) {
|
|
1168
|
+
if (s.payload >= 0 && leads(s.payload)) {
|
|
1169
|
+
proposed.set(s.payload, this.store.bytesPrefix(s.payload, ALL));
|
|
1170
|
+
source.set(s.payload, "recognised site");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const W = this.maxGroup;
|
|
1174
|
+
if (this.host.canonResolve !== undefined && factBytes.length >= W) {
|
|
1175
|
+
const canon = this.host.canonResolve.bind(this.host);
|
|
1176
|
+
for (let start = 0; start < factBytes.length; start++) {
|
|
1177
|
+
for (let end = factBytes.length; end - start >= W; end--) {
|
|
1178
|
+
const id = canon(factBytes.subarray(start, end));
|
|
1179
|
+
if (id === null)
|
|
1180
|
+
continue;
|
|
1181
|
+
if (leads(id)) {
|
|
1182
|
+
proposed.set(id, this.store.bytesPrefix(id, ALL));
|
|
1183
|
+
source.set(id, "canonical fold");
|
|
1184
|
+
}
|
|
1185
|
+
break; // the longest form at this offset wins
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const out = { proposed, source, sites };
|
|
1190
|
+
this.entityMemo.set(memoKey, out);
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
983
1193
|
/** DERIVE-THROUGH — a RULE this module's DeductionSystem was missing. The
|
|
984
1194
|
* A*LD library is untouched: this is one more `premises → conclusion + cost`
|
|
985
1195
|
* rule in the system {@link buildSearch} hands to {@link lightestDerivation},
|
|
@@ -994,19 +1204,22 @@ export class GraphSearch {
|
|
|
994
1204
|
* fact reached through it. On the ladder it is one STEP: a direct edge,
|
|
995
1205
|
* exactly as following a literal continuation is. Deterministic and
|
|
996
1206
|
* point-probed (`resolve` + `nextFirst`, no scan), so it adds no read that
|
|
997
|
-
* grows with the corpus.
|
|
1207
|
+
* grows with the corpus. Asked ONCE per fact a lightest derivation stood on
|
|
1208
|
+
* (the join license, {@link solve}) — never per fact the exploration merely
|
|
1209
|
+
* reached — so the number of facts it prices is the answer's, not the
|
|
1210
|
+
* corpus's. The move is visible in the rationale as its own act
|
|
998
1211
|
* (`classifyMove` reports `derive-through`), distinct from the
|
|
999
1212
|
* byte-concatenating `fuse`/`splice` — and named `derive-through` rather than
|
|
1000
1213
|
* `join` so it cannot be read as the confluence mechanism's `Provenance`. */
|
|
1001
|
-
*deriveThrough(fact, queryBytes, queryLen) {
|
|
1214
|
+
*deriveThrough(fact, queryBytes, queryLen, queryNodes) {
|
|
1002
1215
|
if (!this.host.recogniseSpan)
|
|
1003
1216
|
return;
|
|
1004
1217
|
const tail = queryBytes.subarray(fact.j, queryLen);
|
|
1005
1218
|
if (tail.length === 0)
|
|
1006
1219
|
return;
|
|
1007
|
-
// Report ONLY the invocations that could have joined:
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1220
|
+
// Report ONLY the invocations that could have joined: a derivation stands on
|
|
1221
|
+
// node-bearing outs of every width, including the one-byte outs the cover
|
|
1222
|
+
// bridges with — measured, 68 refusals for a single
|
|
1010
1223
|
// 3-relation query, all of them letters. A form shorter than one window is
|
|
1011
1224
|
// not a fact a join could travel through, so it is not a refusal worth
|
|
1012
1225
|
// reporting; W is the same line the rest of the mind draws between a chance
|
|
@@ -1016,65 +1229,20 @@ export class GraphSearch {
|
|
|
1016
1229
|
// same recogniser the query went through, so the evidence standard is the
|
|
1017
1230
|
// query's. A byte atom is never a subject; the fact's own node is the span
|
|
1018
1231
|
// itself, not an entity inside it. The admission predicate has ONE
|
|
1019
|
-
// definition —
|
|
1020
|
-
//
|
|
1021
|
-
// and a bare host
|
|
1232
|
+
// definition — the store's `leadsSomewhere` (edge or halo); the host LENDS
|
|
1233
|
+
// its memoised form when it can (Mind does, with the response-scoped struct
|
|
1234
|
+
// cache), and a bare host asks the store directly, so the search stays
|
|
1022
1235
|
// host-based.
|
|
1023
|
-
const
|
|
1024
|
-
const leads = (id) => this.host.leadsSomewhere !== undefined
|
|
1025
|
-
? this.host.leadsSomewhere(id)
|
|
1026
|
-
: this.store.hasNext(id) || this.store.hasHalo(id);
|
|
1236
|
+
const { proposed, source, sites: factSites } = this.entityProposals(fact.bytes);
|
|
1027
1237
|
// THE QUERY'S OWN SUBJECT, CANONICALLY. The filter used raw bytes and the
|
|
1028
1238
|
// store's nodes are canonical, so `Eiffel Tower country` in the query did
|
|
1029
1239
|
// not match the deposited `eiffel tower country` — measured, that is the
|
|
1030
|
-
// trap's wrong answer.
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
// a second offset scan. `canonicalQueryNodes` re-derived, per byte offset,
|
|
1035
|
-
// what `recognise` had already resolved once per query (its memo is keyed by
|
|
1036
|
-
// content), and that scan was the largest single cost the DIANOT join added:
|
|
1037
|
-
// measured against the pre-change tree, the same fixture and the same test
|
|
1038
|
-
// were 21 s slower with the scan than without it. A recognised site IS a
|
|
1039
|
-
// canonical node of the query that can lead somewhere, which is exactly the
|
|
1040
|
-
// set this filter wants, and it costs nothing to read.
|
|
1041
|
-
const queryNodes = new Set((this.host.recogniseSpan?.(queryBytes)?.sites ?? []).map((s) => s.payload));
|
|
1042
|
-
// TWO SOURCES, ONE ADMISSION. The recognition of a STORED WHOLE returns the
|
|
1043
|
-
// whole and stops — measured: for `The director of Eva is Gustaf Molander.`
|
|
1044
|
-
// it yields exactly ONE site, the fact's own node — so the entity a join
|
|
1045
|
-
// exists for is never proposed. The canonical fold is the second source,
|
|
1046
|
-
// and the scan runs only for a FORM (≥ W: a one-byte out is not something to
|
|
1047
|
-
// join through, and running it per letter measured 20-26 s in test/99).
|
|
1048
|
-
const W = this.maxGroup;
|
|
1049
|
-
const proposed = new Map();
|
|
1050
|
-
// The SOURCE of each proposal travels with it: a refusal that names only the
|
|
1051
|
-
// bytes leaves the next reader guessing which path proposed them — three
|
|
1052
|
-
// attempts at the chained join were spent fixing paths that never produced
|
|
1053
|
-
// the offending candidate.
|
|
1054
|
-
const source = new Map();
|
|
1055
|
-
for (const s of factRec.sites) {
|
|
1056
|
-
if (s.payload >= 0 && leads(s.payload)) {
|
|
1057
|
-
proposed.set(s.payload, this.store.bytesPrefix(s.payload, ALL));
|
|
1058
|
-
source.set(s.payload, "recognised site");
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
if (this.host.canonResolve !== undefined && fact.bytes.length >= W) {
|
|
1062
|
-
const canon = this.host.canonResolve.bind(this.host);
|
|
1063
|
-
for (let start = 0; start < fact.bytes.length; start++) {
|
|
1064
|
-
for (let end = fact.bytes.length; end - start >= W; end--) {
|
|
1065
|
-
const id = canon(fact.bytes.subarray(start, end));
|
|
1066
|
-
if (id === null)
|
|
1067
|
-
continue;
|
|
1068
|
-
if (leads(id)) {
|
|
1069
|
-
proposed.set(id, this.store.bytesPrefix(id, ALL));
|
|
1070
|
-
source.set(id, "canonical fold");
|
|
1071
|
-
}
|
|
1072
|
-
break; // the longest form at this offset wins
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1240
|
+
// trap's wrong answer. Read from the recognition the response already
|
|
1241
|
+
// computed (`queryNodes`, once per solve), never from a second offset scan:
|
|
1242
|
+
// a per-offset `canonicalQueryNodes` re-derived what `recognise` had
|
|
1243
|
+
// resolved once, and was the largest single cost the join first added.
|
|
1076
1244
|
const leading = [...proposed]
|
|
1077
|
-
.filter(([payload]) => payload !== fact.node && !queryNodes.has(payload))
|
|
1245
|
+
.filter(([payload]) => payload !== fact.node && !queryNodes().has(payload))
|
|
1078
1246
|
.map(([payload, bytes]) => ({ payload, bytes }));
|
|
1079
1247
|
// …then prefer the entity the query did NOT name, and the MAXIMAL one. The
|
|
1080
1248
|
// join exists to reach the subject the query never wrote, so:
|
|
@@ -1093,8 +1261,8 @@ export class GraphSearch {
|
|
|
1093
1261
|
// Report WHAT the recognition returned, not just that nothing led: the
|
|
1094
1262
|
// count and the first few site texts are the difference between "the
|
|
1095
1263
|
// fact was not recognised" and "it was recognised but nothing led".
|
|
1096
|
-
const seen =
|
|
1097
|
-
this.host.reportSearch?.("deriveThroughMiss", [fact.bytes, tail, ...seen], `no entity inside the fact leads anywhere — ${
|
|
1264
|
+
const seen = factSites.slice(0, 3).map((s) => this.store.bytesPrefix(s.payload, ALL));
|
|
1265
|
+
this.host.reportSearch?.("deriveThroughMiss", [fact.bytes, tail, ...seen], `no entity inside the fact leads anywhere — ${factSites.length} site(s) recognised inside it`);
|
|
1098
1266
|
}
|
|
1099
1267
|
}
|
|
1100
1268
|
const candidates = leading
|
|
@@ -1199,12 +1367,12 @@ export class GraphSearch {
|
|
|
1199
1367
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
|
1200
1368
|
* learnt connector (the in-search bridge), splitting (at a sub-leaf form
|
|
1201
1369
|
* boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
|
|
1202
|
-
* finalised out, and — for a
|
|
1203
|
-
* with the query's tail ({@link deriveThrough}). */
|
|
1370
|
+
* finalised out, and — for a LICENSED fact — JOINING the entity it contains
|
|
1371
|
+
* with the query's tail ({@link deriveThrough}, granted in {@link solve}). */
|
|
1204
1372
|
*outRules(it, ctx) {
|
|
1205
1373
|
const { splits, coversDone, outsByStart, outsByEnd, coverableByStart } = ctx;
|
|
1206
1374
|
const outsByNode = ctx.outsByNode;
|
|
1207
|
-
const byRight = ctx.linksByRight
|
|
1375
|
+
const byRight = ctx.linksByRight;
|
|
1208
1376
|
pushInto(outsByStart, it.i, it);
|
|
1209
1377
|
pushInto(outsByEnd, it.j, it);
|
|
1210
1378
|
if (it.rec && it.node !== undefined)
|
|
@@ -1221,24 +1389,26 @@ export class GraphSearch {
|
|
|
1221
1389
|
// Points 2 & 5) — but NEVER across the asker's own unrecognised separator (a
|
|
1222
1390
|
// space, comma), so "ice fire" stays "cold hot", never "cold or hot".
|
|
1223
1391
|
//
|
|
1224
|
-
// Cost stays bounded by iterating only the FEW
|
|
1392
|
+
// Cost stays bounded by iterating only the FEW offered connector targets of
|
|
1225
1393
|
// this out's node (links are selective and, for the N-ary case, keyed first→
|
|
1226
1394
|
// later — O(parts), not O(parts²)), and matching them against finalised outs
|
|
1227
|
-
// by node id, rather than scanning every position pair.
|
|
1395
|
+
// by node id, rather than scanning every position pair. A pair is bridged
|
|
1396
|
+
// only once its two premises have met here ({@link ConnectorLicence}).
|
|
1228
1397
|
const byLeft = ctx.linksByLeft;
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1398
|
+
const licence = ctx.connectors;
|
|
1399
|
+
if (licence && it.rec && it.node !== undefined) {
|
|
1400
|
+
// L = this out, R = a later out whose node is an offered target.
|
|
1401
|
+
for (const [rNode, key] of byLeft.get(it.node) ?? []) {
|
|
1232
1402
|
for (const r of outsByNode.get(rNode) ?? []) {
|
|
1233
|
-
const rule = this.trySplice(it, r,
|
|
1403
|
+
const rule = this.trySplice(it, r, key, licence, outsByEnd);
|
|
1234
1404
|
if (rule)
|
|
1235
1405
|
yield rule;
|
|
1236
1406
|
}
|
|
1237
1407
|
}
|
|
1238
|
-
// R = this out, L = an earlier out whose node has
|
|
1239
|
-
for (const [lNode,
|
|
1408
|
+
// R = this out, L = an earlier out whose node has an offered target here.
|
|
1409
|
+
for (const [lNode, key] of byRight.get(it.node) ?? []) {
|
|
1240
1410
|
for (const l of outsByNode.get(lNode) ?? []) {
|
|
1241
|
-
const rule = this.trySplice(l, it,
|
|
1411
|
+
const rule = this.trySplice(l, it, key, licence, outsByEnd);
|
|
1242
1412
|
if (rule)
|
|
1243
1413
|
yield rule;
|
|
1244
1414
|
}
|
|
@@ -1300,10 +1470,11 @@ export class GraphSearch {
|
|
|
1300
1470
|
// continuation is the derived answer — a genuine relational join, distinct
|
|
1301
1471
|
// from the confluence mechanism's `join` PROVENANCE — and not the
|
|
1302
1472
|
// juxtaposition the cover produces when the intermediate key IS named.
|
|
1303
|
-
//
|
|
1304
|
-
// the
|
|
1305
|
-
|
|
1306
|
-
|
|
1473
|
+
// On the ladder and memoised by {@link key} like every rule, but fired only
|
|
1474
|
+
// for a fact the solve has LICENSED (a fact a lightest derivation stood on —
|
|
1475
|
+
// see {@link solve}), from the joins computed for it once.
|
|
1476
|
+
for (const g of ctx.joins.get(chartKey(it)) ?? []) {
|
|
1477
|
+
yield { premises: [it], conclusion: g.conclusion, cost: g.cost };
|
|
1307
1478
|
}
|
|
1308
1479
|
}
|
|
1309
1480
|
/** Whether the query span [from, to) is wholly covered by RECOGNISED outs —
|