@hviana/sema 0.8.9 → 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 +27 -0
- package/dist/src/meter.js +28 -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 +55 -73
- 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 +28 -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 +49 -65
- 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
package/src/mind/graph-search.ts
CHANGED
|
@@ -87,6 +87,11 @@ export type GItem =
|
|
|
87
87
|
* consolidated, more-explanatory reading wins over leaving the parts split.
|
|
88
88
|
* See {@link GraphSearch.fuse} and {@link GraphSearch.formRules}. */
|
|
89
89
|
rcmp?: boolean;
|
|
90
|
+
/** Set on a concept hop's ASKING form: the edge-less form `node` whose halo
|
|
91
|
+
* target the caller has not granted yet, held at the hop's own cost and
|
|
92
|
+
* span. Popping it asks for the target; it leads nowhere itself, so it
|
|
93
|
+
* is never part of a final derivation (see {@link Licence}). */
|
|
94
|
+
ask?: boolean;
|
|
90
95
|
}
|
|
91
96
|
| {
|
|
92
97
|
kind: "out";
|
|
@@ -126,6 +131,13 @@ export type GItem =
|
|
|
126
131
|
};
|
|
127
132
|
type OutItem = Extract<GItem, { kind: "out" }>;
|
|
128
133
|
|
|
134
|
+
/** What {@link GraphSearch.entityProposals} found inside one fact. */
|
|
135
|
+
interface EntityProposals {
|
|
136
|
+
readonly proposed: ReadonlyMap<number, Uint8Array>;
|
|
137
|
+
readonly source: ReadonlyMap<number, string>;
|
|
138
|
+
readonly sites: ReadonlyArray<Site>;
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
// The cost ladder is a strict ORDERING, not tuned magic:
|
|
130
142
|
// • Coverage dominates everything: leaving one query byte unrecognised (PASS)
|
|
131
143
|
// outweighs any chain of graph steps a covering derivation could take, so
|
|
@@ -174,9 +186,6 @@ export interface Seg {
|
|
|
174
186
|
fix?: boolean;
|
|
175
187
|
}
|
|
176
188
|
|
|
177
|
-
/** Read the chosen spans back off a derivation: the goal is a chain of bridge
|
|
178
|
-
* steps, each whose second premise is the `out` it crossed. Walk the chain to
|
|
179
|
-
* the axiom and reverse into left-to-right order. */
|
|
180
189
|
/** The BYTE TERM of a derivation's cost: how many bytes its bridge rule charged
|
|
181
190
|
* at PASS each — read back off the rule that charged them (`bridgeRule`:
|
|
182
191
|
* `o.rec ? MICRO : PASS * (o.j - o.i)`), so the split exists in ONE place and
|
|
@@ -201,6 +210,9 @@ function readBridgedBytes(derivation: Derivation<GItem>): number {
|
|
|
201
210
|
return bytes;
|
|
202
211
|
}
|
|
203
212
|
|
|
213
|
+
/** Read the chosen spans back off a derivation: the goal is a chain of bridge
|
|
214
|
+
* steps, each whose second premise is the `out` it crossed. Walk the chain to
|
|
215
|
+
* the axiom and reverse into left-to-right order. */
|
|
204
216
|
function readCover(derivation: Derivation<GItem>): Seg[] {
|
|
205
217
|
const segs: Seg[] = [];
|
|
206
218
|
let node: Derivation<GItem> | undefined = derivation;
|
|
@@ -258,10 +270,6 @@ export interface DerivationItem {
|
|
|
258
270
|
node?: number;
|
|
259
271
|
}
|
|
260
272
|
|
|
261
|
-
/** The reasoning act a derivation rule performs — the human name for which of
|
|
262
|
-
* {@link GraphSearch}'s rules fired, recovered from the rule's premise/
|
|
263
|
-
* conclusion shape (the rules carry no label, so this classifies by structure,
|
|
264
|
-
* the single place that maps rule geometry to a name). */
|
|
265
273
|
// CLOSED ON PURPOSE — AND ONLY THIS ONE IS. A derivation move is BRANCHED ON
|
|
266
274
|
// (`classifyMove`, the rationale's readers, MOVE_NOTE's fallback), so it is a
|
|
267
275
|
// closed union: adding one without teaching every reader is a compile error,
|
|
@@ -270,6 +278,10 @@ export interface DerivationItem {
|
|
|
270
278
|
// branched on — and they stay free strings that COMPOSE with the nesting
|
|
271
279
|
// (`["respond", "think", "recognise"]`). That asymmetry is deliberate; do not
|
|
272
280
|
// "fix" it by uniting the two (see test/126 for the pipeline half of it).
|
|
281
|
+
/** The reasoning act a derivation rule performs — the human name for which of
|
|
282
|
+
* {@link GraphSearch}'s rules fired, recovered from the rule's premise/
|
|
283
|
+
* conclusion shape (the rules carry no label, so this classifies by structure,
|
|
284
|
+
* the single place that maps rule geometry to a name). */
|
|
273
285
|
export type DerivationMove =
|
|
274
286
|
| "axiom" // a seed: a perceived leaf, a recognised form, or a computed result
|
|
275
287
|
| "follow-edge" // form→form via a continuation edge (STEP) — the core "what follows what"
|
|
@@ -285,6 +297,93 @@ export type DerivationMove =
|
|
|
285
297
|
| "pool-vote" // N premises→conclusion, evidence pooled (combine:"sum" — see derive)
|
|
286
298
|
| "step"; // any other single-premise move (fallback)
|
|
287
299
|
|
|
300
|
+
/** The chart's canonical key — an item's boundary signature (see
|
|
301
|
+
* {@link DeductionSystem.key}). Module-level because the join license
|
|
302
|
+
* ({@link GraphSearch.solve}) names facts by the same key the chart does. */
|
|
303
|
+
function chartKey(it: GItem): string {
|
|
304
|
+
if (it.kind === "cover") return "c" + it.p;
|
|
305
|
+
if (it.kind === "form") {
|
|
306
|
+
return `f${it.i}.${it.j}.${it.node}.${it.via ? 1 : 0}.${it.rcmp ? 1 : 0}${
|
|
307
|
+
it.ask ? "?" : ""
|
|
308
|
+
}`;
|
|
309
|
+
}
|
|
310
|
+
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${
|
|
311
|
+
it.fix ? 1 : 0
|
|
312
|
+
}.${it.throughFact ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The facts a derivation STANDS ON — every node-bearing `out` in its proof
|
|
316
|
+
* tree, premises before conclusions, each once. */
|
|
317
|
+
function factsOf(root: Derivation<GItem>): OutItem[] {
|
|
318
|
+
const out: OutItem[] = [];
|
|
319
|
+
const seen = new Set<Derivation<GItem>>();
|
|
320
|
+
const walk = (d: Derivation<GItem>): void => {
|
|
321
|
+
if (seen.has(d)) return;
|
|
322
|
+
seen.add(d);
|
|
323
|
+
for (const p of d.premises) walk(p);
|
|
324
|
+
if (d.item.kind === "out" && d.item.node !== undefined) out.push(d.item);
|
|
325
|
+
};
|
|
326
|
+
walk(root);
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The joins granted to the facts a derivation stood on: fact chart key → the
|
|
331
|
+
* conclusions {@link GraphSearch.deriveThrough} derives through it, computed
|
|
332
|
+
* once per fact (an empty list is an answer: that fact joins nothing). */
|
|
333
|
+
type JoinLicense = Map<
|
|
334
|
+
string,
|
|
335
|
+
ReadonlyArray<{ conclusion: GItem; cost: number }>
|
|
336
|
+
>;
|
|
337
|
+
|
|
338
|
+
/** THE ASYNC PREMISES A COVER USES, RESOLVED ONLY WHERE THE SEARCH REACHES THEM.
|
|
339
|
+
*
|
|
340
|
+
* Two of the cover's rules need data the synchronous search cannot fetch — a
|
|
341
|
+
* connector splice (a bridge between two answers) and a concept hop (an
|
|
342
|
+
* edge-less form's halo sibling) — and both used to be resolved for EVERY
|
|
343
|
+
* candidate before the search ran. A rule fires only from popped premises,
|
|
344
|
+
* and the agenda pops in cost order up to the goal, so most of that work fed
|
|
345
|
+
* rules the search never reached: measured on the 31.7M-node store, a 262-byte
|
|
346
|
+
* query recognised whole paid 23 bridges (143,520 junction pops, 7.3 s of its
|
|
347
|
+
* 7.9 s) for sub-forms whose rewrites cost a STEP each and so never left the
|
|
348
|
+
* agenda before the one-STEP goal — and then 1.2 s of halo reads for concept
|
|
349
|
+
* hops priced at CONCEPT, ten times that goal.
|
|
350
|
+
*
|
|
351
|
+
* So the caller OFFERS what it can resolve; where the search reaches an offered
|
|
352
|
+
* key it uses a GRANTED value and ASKS for an ungranted one — a connector when
|
|
353
|
+
* its splice's premises meet, a concept target when the hop's conclusion, held
|
|
354
|
+
* as an asking form at the hop's own cost, is popped. A run that asked
|
|
355
|
+
* nothing is the run every key resolved in advance would have made: the pops
|
|
356
|
+
* are decided only by the rules fired from popped items, a granted key fires
|
|
357
|
+
* the very rule it fired before, and an asking form that was never popped fired
|
|
358
|
+
* none. So its cover is final. A run that asked is PROVISIONAL: the caller
|
|
359
|
+
* grants the asked keys (the async reads the search cannot run) and covers
|
|
360
|
+
* again; every round grants at least one offered key, so the rounds end. */
|
|
361
|
+
export interface Licence<K, V> {
|
|
362
|
+
/** Every key the caller can resolve. */
|
|
363
|
+
readonly offered: ReadonlySet<K>;
|
|
364
|
+
/** The keys resolved so far: their value, or null (nothing learnt). */
|
|
365
|
+
readonly granted: ReadonlyMap<K, V | null>;
|
|
366
|
+
/** Filled by the search: the offered keys it reached ungranted. */
|
|
367
|
+
readonly asked: Set<K>;
|
|
368
|
+
}
|
|
369
|
+
/** Connectors, keyed `L,R` by answer-node ids, valued by the learnt glue. */
|
|
370
|
+
export type ConnectorLicence = Licence<string, Uint8Array>;
|
|
371
|
+
/** Concept hops, keyed by an edge-less form's node, valued by the node its
|
|
372
|
+
* halo sibling's edge leads to. */
|
|
373
|
+
export type ConceptLicence = Licence<number, number>;
|
|
374
|
+
/** Whether a run asked for anything — its cover is then provisional. */
|
|
375
|
+
function asked(
|
|
376
|
+
concepts: ConceptLicence,
|
|
377
|
+
connectors: ConnectorLicence | undefined,
|
|
378
|
+
): boolean {
|
|
379
|
+
return concepts.asked.size > 0 || (connectors?.asked.size ?? 0) > 0;
|
|
380
|
+
}
|
|
381
|
+
/** Nothing offered: a cover with no async premises (a nested completion,
|
|
382
|
+
* articulation's revoicing). */
|
|
383
|
+
export function noLicence<K, V>(): Licence<K, V> {
|
|
384
|
+
return { offered: new Set(), granted: new Map(), asked: new Set() };
|
|
385
|
+
}
|
|
386
|
+
|
|
288
387
|
/** Flatten a {@link GItem} into the {@link DerivationItem} a rationale shows. */
|
|
289
388
|
function derivationItem(it: GItem): DerivationItem {
|
|
290
389
|
if (it.kind === "cover") return { kind: "cover", span: [it.p, it.p] };
|
|
@@ -407,18 +506,9 @@ export class GraphSearch {
|
|
|
407
506
|
private readonly host: GraphSearchHost,
|
|
408
507
|
) {}
|
|
409
508
|
|
|
410
|
-
/** The
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* a join that filters the query's own subject by RAW bytes re-admits it —
|
|
414
|
-
* measured: that is the trap's wrong answer (`The capital of Eiffel Tower
|
|
415
|
-
* country is Berlin.`). Cached by query identity, because the search is
|
|
416
|
-
* reused across responses. */
|
|
417
|
-
|
|
418
|
-
/* * The hub bound √N (bounded-reads.md) — the ONE
|
|
419
|
-
* fan-out cap, stated here rather than imported from `traverse.ts` because
|
|
420
|
-
* this module is deliberately host-based (it holds a bare Store, never a
|
|
421
|
-
* MindContext).
|
|
509
|
+
/** The hub bound √N (bounded-reads.md) — the ONE fan-out cap, stated here
|
|
510
|
+
* rather than imported from `traverse.ts` because this module is
|
|
511
|
+
* deliberately host-based (it holds a bare Store, never a MindContext).
|
|
422
512
|
* That is the same write/read-side duplication convention canonical.ts's
|
|
423
513
|
* header documents: if the formula changes it must change in BOTH places.
|
|
424
514
|
* It is stated ONCE per side, though — the expression used to be spelled
|
|
@@ -443,15 +533,17 @@ export class GraphSearch {
|
|
|
443
533
|
*
|
|
444
534
|
* Any learnt connector between two rewrites is spliced IN by the in-search
|
|
445
535
|
* connector rule (see {@link outRules}), so the returned spans already carry
|
|
446
|
-
* it — there is no post-pass.
|
|
536
|
+
* it — there is no post-pass. With a {@link ConnectorLicence}, a cover
|
|
537
|
+
* returned while `connectors.asked` is non-empty is PROVISIONAL: grant the
|
|
538
|
+
* asked pairs and cover again. */
|
|
447
539
|
cover(
|
|
448
540
|
queryLen: number,
|
|
449
541
|
sites: ReadonlyArray<Site>,
|
|
450
|
-
|
|
542
|
+
concepts: ConceptLicence,
|
|
451
543
|
leaves: ReadonlyArray<Leaf>,
|
|
452
544
|
splits: ReadonlySet<number>,
|
|
453
545
|
substitutions?: ReadonlyMap<number, Uint8Array>,
|
|
454
|
-
connectors?:
|
|
546
|
+
connectors?: ConnectorLicence,
|
|
455
547
|
computedResults?: ReadonlyArray<ComputedResult>,
|
|
456
548
|
/** When given, receives each solved span's lightest derivation — the full
|
|
457
549
|
* adapted A*LD proof tree as classified {@link DerivationStep}s — for the
|
|
@@ -466,14 +558,17 @@ export class GraphSearch {
|
|
|
466
558
|
// through (completion is cover, recursively — see {@link recompleteNode}).
|
|
467
559
|
this.recompleteOpen.clear();
|
|
468
560
|
this.recompleteMemo = new Map<number, Uint8Array | null>();
|
|
561
|
+
this.entityMemo = new Map<string, EntityProposals>();
|
|
469
562
|
// The top cover's derivation sink is threaded into every nested completion
|
|
470
563
|
// so the recompositions a produced form needs are reported in the same
|
|
471
564
|
// trace instead of vanishing after the first layer.
|
|
472
565
|
this.derivationSink = onDerivation;
|
|
566
|
+
concepts.asked.clear();
|
|
567
|
+
if (connectors) connectors.asked.clear();
|
|
473
568
|
const solved = this.solve(
|
|
474
569
|
queryLen,
|
|
475
570
|
{ sites, leaves, splits },
|
|
476
|
-
|
|
571
|
+
concepts,
|
|
477
572
|
substitutions,
|
|
478
573
|
connectors,
|
|
479
574
|
computedResults,
|
|
@@ -486,7 +581,9 @@ export class GraphSearch {
|
|
|
486
581
|
// continuations instead of following the chain the answer itself licensed.
|
|
487
582
|
// With deepening only at the top, `recompleteNode` walks the accepted chain
|
|
488
583
|
// one link at a time (its own memo and stack), so the work is the answer's.
|
|
489
|
-
|
|
584
|
+
// A provisional cover is not deepened: it is about to be re-covered.
|
|
585
|
+
if (solved === null || asked(concepts, connectors)) return solved;
|
|
586
|
+
return {
|
|
490
587
|
segs: this.deepen(solved.segs),
|
|
491
588
|
cost: solved.cost,
|
|
492
589
|
moves: solved.moves,
|
|
@@ -512,32 +609,94 @@ export class GraphSearch {
|
|
|
512
609
|
leaves: ReadonlyArray<Leaf>;
|
|
513
610
|
splits: ReadonlySet<number>;
|
|
514
611
|
},
|
|
515
|
-
|
|
612
|
+
concepts: ConceptLicence,
|
|
516
613
|
substitutions?: ReadonlyMap<number, Uint8Array>,
|
|
517
|
-
connectors?:
|
|
614
|
+
connectors?: ConnectorLicence,
|
|
518
615
|
computedResults?: ReadonlyArray<ComputedResult>,
|
|
519
616
|
onDerivation?: (steps: DerivationStep[]) => void,
|
|
520
617
|
): { segs: Seg[]; cost: number; moves: number } | null {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
618
|
+
// The query's own bytes, tiled from its perceived leaves. A JOIN reads the
|
|
619
|
+
// tail a produced fact's contained entity has to combine with, and the
|
|
620
|
+
// search otherwise only ever sees positions, never the bytes behind them.
|
|
621
|
+
const queryBytes = new Uint8Array(spanLen);
|
|
622
|
+
for (const lf of recognition.leaves) {
|
|
623
|
+
queryBytes.set(lf.bytes.subarray(0, lf.end - lf.start), lf.start);
|
|
624
|
+
}
|
|
625
|
+
// The nodes this span's recognition names — read ONCE per solve, on the first
|
|
626
|
+
// join that asks, instead of re-reading the memo per fact.
|
|
627
|
+
let queryNodeSet: ReadonlySet<number> | undefined;
|
|
628
|
+
const queryNodes = (): ReadonlySet<number> =>
|
|
629
|
+
queryNodeSet ??= new Set<number>(
|
|
630
|
+
(this.host.recogniseSpan?.(queryBytes)?.sites ?? []).map((s) =>
|
|
631
|
+
s.payload
|
|
632
|
+
),
|
|
633
|
+
);
|
|
634
|
+
// THE JOIN DERIVES THROUGH THE FACTS THE DERIVATION STANDS ON — never
|
|
635
|
+
// through every fact the exploration REACHES. How many facts the search
|
|
636
|
+
// reaches is set by how densely the corpus interconnects the forms on the
|
|
637
|
+
// way (a dialogue hub's continuations, walked to their fixpoints, are
|
|
638
|
+
// thousands), and pricing a join from each of them made the work track the
|
|
639
|
+
// corpus instead of the answer: measured on the 31.7M-node store, a 46-byte
|
|
640
|
+
// query reached 2,000+ fixpoints in a minute, each paying an entity scan,
|
|
641
|
+
// past 10 GB. It is the trap {@link recompleteNode} already records, and
|
|
642
|
+
// the same cure as {@link deepen}: the expensive step runs on what the
|
|
643
|
+
// lightest derivation CHOSE.
|
|
644
|
+
//
|
|
645
|
+
// So the search runs to its lightest derivation, the facts that derivation
|
|
646
|
+
// stands on are LICENSED — their joins computed once — and it runs again
|
|
647
|
+
// with those joins as ordinary rules of the same system, priced exactly as
|
|
648
|
+
// before (the fact's own cost + STEP). A join's conclusion is itself a fact
|
|
649
|
+
// the next derivation may stand on, so chains extend round by round until
|
|
650
|
+
// no newly licensed fact joins anything: the depth is the answer's, never a
|
|
651
|
+
// count. A round is re-run only when a newly licensed fact DOES join, so a
|
|
652
|
+
// query whose facts join nothing pays one search, as before.
|
|
653
|
+
//
|
|
654
|
+
// The joins a covering round licensed are KEPT across a licence's re-covers:
|
|
655
|
+
// a round that asked for nothing is the very round the next re-cover repeats
|
|
656
|
+
// (see {@link Licence}), so its joins are the ones that re-cover would grant
|
|
657
|
+
// again.
|
|
658
|
+
const joins: JoinLicense = this.joinsKept.get(concepts) ?? new Map();
|
|
659
|
+
this.joinsKept.set(concepts, joins);
|
|
531
660
|
// Search-effort accounting (src/meter.ts): the chart's pops/pushes are
|
|
532
661
|
// the cover's real cost, and a heuristic that stops being admissible
|
|
533
662
|
// shows up as a pop count that explodes while the answer stays the same.
|
|
534
663
|
const meter = this.host.meter;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
664
|
+
let derivation: Derivation<GItem> | null;
|
|
665
|
+
for (;;) {
|
|
666
|
+
const system = this.buildSearch(
|
|
667
|
+
spanLen,
|
|
668
|
+
recognition.sites,
|
|
669
|
+
concepts,
|
|
670
|
+
recognition.leaves,
|
|
671
|
+
recognition.splits,
|
|
672
|
+
queryBytes,
|
|
673
|
+
joins,
|
|
674
|
+
substitutions,
|
|
675
|
+
connectors,
|
|
676
|
+
computedResults,
|
|
677
|
+
);
|
|
678
|
+
const stats = meter ? { pops: 0, pushes: 0 } : undefined;
|
|
679
|
+
derivation = lightestDerivation(system, stats);
|
|
680
|
+
if (meter && stats) {
|
|
681
|
+
meter.searches++;
|
|
682
|
+
meter.searchPops += stats.pops;
|
|
683
|
+
meter.searchPushes += stats.pushes;
|
|
684
|
+
}
|
|
685
|
+
// A round that ASKED for a connector is provisional: return it before any
|
|
686
|
+
// join is licensed from a derivation the granted connectors may change.
|
|
687
|
+
if (derivation === null || asked(concepts, connectors)) break;
|
|
688
|
+
let joined = false;
|
|
689
|
+
for (const fact of factsOf(derivation)) {
|
|
690
|
+
const k = chartKey(fact);
|
|
691
|
+
if (joins.has(k)) continue;
|
|
692
|
+
if (meter) meter.joinFacts++;
|
|
693
|
+
const granted = [
|
|
694
|
+
...this.deriveThrough(fact, queryBytes, spanLen, queryNodes),
|
|
695
|
+
].map((r) => ({ conclusion: r.conclusion, cost: r.cost }));
|
|
696
|
+
joins.set(k, granted);
|
|
697
|
+
if (granted.length > 0) joined = true;
|
|
698
|
+
}
|
|
699
|
+
if (!joined) break;
|
|
541
700
|
}
|
|
542
701
|
// When covering under a substitution map (articulation), a form→out rule is
|
|
543
702
|
// the form EMITTING the asker's voice, not grounding to its own answer — so
|
|
@@ -587,11 +746,13 @@ export class GraphSearch {
|
|
|
587
746
|
private buildSearch(
|
|
588
747
|
queryLen: number,
|
|
589
748
|
sites: ReadonlyArray<Site>,
|
|
590
|
-
|
|
749
|
+
concepts: ConceptLicence,
|
|
591
750
|
leaves: ReadonlyArray<Leaf>,
|
|
592
751
|
splits: ReadonlySet<number>,
|
|
752
|
+
queryBytes: Uint8Array,
|
|
753
|
+
joins: JoinLicense,
|
|
593
754
|
substitutions?: ReadonlyMap<number, Uint8Array>,
|
|
594
|
-
connectors?:
|
|
755
|
+
connectors?: ConnectorLicence,
|
|
595
756
|
computedResults?: ReadonlyArray<ComputedResult>,
|
|
596
757
|
): DeductionSystem<GItem> {
|
|
597
758
|
const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
|
|
@@ -603,13 +764,6 @@ export class GraphSearch {
|
|
|
603
764
|
Math.ceil((this.store.edgeSourceCount() * W) / 256),
|
|
604
765
|
) > this.hubBound();
|
|
605
766
|
const nodeBytes = (n: number) => this.store.bytesPrefix(n, ALL);
|
|
606
|
-
// The query's own bytes, tiled from its perceived leaves. A JOIN reads the
|
|
607
|
-
// tail a produced fact's contained entity has to combine with, and `buildSearch`
|
|
608
|
-
// otherwise only ever sees positions, never the bytes behind them.
|
|
609
|
-
const queryBytes = new Uint8Array(queryLen);
|
|
610
|
-
for (const lf of leaves) {
|
|
611
|
-
queryBytes.set(lf.bytes.subarray(0, lf.end - lf.start), lf.start);
|
|
612
|
-
}
|
|
613
767
|
// Content-addressed probes over the store's hash-cons maps — the same keys
|
|
614
768
|
// training filled. No byte-by-byte trie walk.
|
|
615
769
|
const findLeafU = (b: Uint8Array) => this.store.findLeaf(b) ?? undefined;
|
|
@@ -622,34 +776,82 @@ export class GraphSearch {
|
|
|
622
776
|
const outsByNode = new Map<number, OutItem[]>();
|
|
623
777
|
const coverableByStart = new Map<number, OutItem[]>();
|
|
624
778
|
|
|
625
|
-
// Index the connectors by their left and right answer-node, so the
|
|
626
|
-
// rule iterates only this out's FEW
|
|
627
|
-
// N-ary case O(parts) keys) instead of scanning every position
|
|
628
|
-
// keeps the in-search bridge bounded when many parts are
|
|
629
|
-
|
|
630
|
-
const
|
|
779
|
+
// Index the offered connectors by their left and right answer-node, so the
|
|
780
|
+
// connector rule iterates only this out's FEW offered partners (selective,
|
|
781
|
+
// and for the N-ary case O(parts) keys) instead of scanning every position
|
|
782
|
+
// pair — what keeps the in-search bridge bounded when many parts are
|
|
783
|
+
// recognised at once.
|
|
784
|
+
const linksByLeft = new Map<number, Array<[number, string]>>();
|
|
785
|
+
const linksByRight = new Map<number, Array<[number, string]>>();
|
|
631
786
|
if (connectors) {
|
|
632
|
-
for (const
|
|
787
|
+
for (const key of connectors.offered) {
|
|
633
788
|
const comma = key.indexOf(",");
|
|
634
789
|
const l = Number(key.slice(0, comma));
|
|
635
790
|
const r = Number(key.slice(comma + 1));
|
|
636
|
-
pushInto(linksByLeft, l, [r,
|
|
637
|
-
pushInto(linksByRight, r, [l,
|
|
791
|
+
pushInto(linksByLeft, l, [r, key]);
|
|
792
|
+
pushInto(linksByRight, r, [l, key]);
|
|
638
793
|
}
|
|
639
794
|
}
|
|
640
795
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
796
|
+
// A SPAN'S CHEAPEST COMPLETION DOMINATES THE REST. Coverage is positional:
|
|
797
|
+
// every recognised completion of [i, j) advances the cover from i to j at
|
|
798
|
+
// the same MICRO, so for the goal only the cheapest matters. What else a
|
|
799
|
+
// completion can do turns on its BYTES — fuse it with a neighbour, splice a
|
|
800
|
+
// connector, join through it — and those rules fire from the completion the
|
|
801
|
+
// search would stand on for that span, never from every alternative it
|
|
802
|
+
// reached: the cure {@link deepen} and the join license already apply, for
|
|
803
|
+
// the same trap. Measured on the 31.7M-node store, a 46-byte dialogue query
|
|
804
|
+
// reached 60,530 continuation forms and 57,072 fixpoints for its few spans —
|
|
805
|
+
// a greeting's thousands of replies, each a fixpoint at the same depth — and
|
|
806
|
+
// canonically resolved every one of them fused with its neighbours (5.4 s
|
|
807
|
+
// of an 8.6 s cover) to stand, in the end, on two hops.
|
|
808
|
+
//
|
|
809
|
+
// So a form or completion of [i, j) whose cost has reached that of a
|
|
810
|
+
// completion of [i, j) already yielded is DOMINATED and fires nothing: every
|
|
811
|
+
// completion it could lead to costs at least as much (local costs are
|
|
812
|
+
// non-negative and a form's rules keep its span), and a tie goes to the one
|
|
813
|
+
// yielded first — the evidence-preferred continuation, yielded first by
|
|
814
|
+
// {@link formRules}. This is also what makes the stop-here offer a real
|
|
815
|
+
// horizon: a chain deeper than its first hop's stop (STEP + CONCEPT) is
|
|
816
|
+
// dominated by it, where before the PASS-priced goal let the search buy a
|
|
817
|
+
// thousand hops to look for one more byte.
|
|
818
|
+
const spanBest = new Map<number, { cost: number; item: GItem }>();
|
|
819
|
+
const spanOf = (i: number, j: number) => i * (queryLen + 1) + j;
|
|
820
|
+
const completes = (it: GItem): it is OutItem =>
|
|
821
|
+
it.kind === "out" && it.rec && it.cover;
|
|
822
|
+
const dominated = (it: GItem, cost: number): boolean => {
|
|
823
|
+
if (it.kind === "cover") return false;
|
|
824
|
+
if (it.kind === "out" && !completes(it)) return false;
|
|
825
|
+
const best = spanBest.get(spanOf(it.i, it.j));
|
|
826
|
+
if (best === undefined || best.cost > cost) {
|
|
827
|
+
if (completes(it)) spanBest.set(spanOf(it.i, it.j), { cost, item: it });
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
return best.item !== it &&
|
|
831
|
+
(it.kind !== "out" || chartKey(best.item) !== chartKey(it));
|
|
832
|
+
};
|
|
833
|
+
const completing = function* (
|
|
834
|
+
rules: Iterable<Rule<GItem>>,
|
|
835
|
+
costOf: (it: GItem) => number,
|
|
836
|
+
): Iterable<Rule<GItem>> {
|
|
837
|
+
for (const r of rules) {
|
|
838
|
+
const c = r.conclusion;
|
|
839
|
+
if (completes(c)) {
|
|
840
|
+
let cost = r.cost;
|
|
841
|
+
for (const p of r.premises) cost += costOf(p);
|
|
842
|
+
const s = spanOf(c.i, c.j);
|
|
843
|
+
const best = spanBest.get(s);
|
|
844
|
+
if (best === undefined || cost < best.cost) {
|
|
845
|
+
spanBest.set(s, { cost, item: c });
|
|
846
|
+
}
|
|
648
847
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
848
|
+
yield r;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const meter = this.host.meter;
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
key: chartKey,
|
|
653
855
|
*axioms() {
|
|
654
856
|
yield { item: { kind: "cover", p: 0 }, cost: 0 };
|
|
655
857
|
// One out per tree leaf — content-defined chunks, far fewer than bytes.
|
|
@@ -735,35 +937,39 @@ export class GraphSearch {
|
|
|
735
937
|
const right = it.kind === "cover" ? it.p : it.j;
|
|
736
938
|
return (queryLen - right) * MICRO;
|
|
737
939
|
},
|
|
738
|
-
rules: (it) => {
|
|
940
|
+
rules: (it, costOf) => {
|
|
739
941
|
if (it.kind === "cover") {
|
|
740
942
|
return this.coverRules(it, coversDone, coverableByStart);
|
|
741
943
|
}
|
|
944
|
+
if (dominated(it, costOf(it))) {
|
|
945
|
+
if (meter) meter.searchDominated++;
|
|
946
|
+
return [];
|
|
947
|
+
}
|
|
742
948
|
if (it.kind === "form") {
|
|
743
|
-
return
|
|
744
|
-
it,
|
|
745
|
-
|
|
746
|
-
substitutions,
|
|
747
|
-
nodeBytes,
|
|
748
|
-
queryLen,
|
|
949
|
+
return completing(
|
|
950
|
+
this.formRules(it, concepts, substitutions, nodeBytes),
|
|
951
|
+
costOf,
|
|
749
952
|
);
|
|
750
953
|
}
|
|
751
|
-
return
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
954
|
+
return completing(
|
|
955
|
+
this.outRules(it, {
|
|
956
|
+
W,
|
|
957
|
+
splits,
|
|
958
|
+
atomsAreHubs,
|
|
959
|
+
coversDone,
|
|
960
|
+
outsByStart,
|
|
961
|
+
outsByEnd,
|
|
962
|
+
outsByNode,
|
|
963
|
+
coverableByStart,
|
|
964
|
+
findLeafU,
|
|
965
|
+
findBranchU,
|
|
966
|
+
linksByLeft,
|
|
967
|
+
linksByRight,
|
|
968
|
+
connectors,
|
|
969
|
+
joins,
|
|
970
|
+
}),
|
|
971
|
+
costOf,
|
|
972
|
+
);
|
|
767
973
|
},
|
|
768
974
|
};
|
|
769
975
|
}
|
|
@@ -815,19 +1021,24 @@ export class GraphSearch {
|
|
|
815
1021
|
|
|
816
1022
|
/** The connector-SPLICE rule for an oriented (l, r) pair, or null when the
|
|
817
1023
|
* pair does not qualify — the ONE body behind {@link outRules}' two
|
|
818
|
-
* mirror loops (this-as-left over
|
|
819
|
-
* over
|
|
1024
|
+
* mirror loops (this-as-left over offered right partners, this-as-right
|
|
1025
|
+
* over offered left partners). Fires only when both sides are
|
|
820
1026
|
* recognised, r starts at or after l ends, and the gap between them is
|
|
821
1027
|
* empty or wholly recognised — never across the asker's own literal
|
|
822
|
-
* separator.
|
|
1028
|
+
* separator — and the pair's connector is granted and learnt. A qualifying
|
|
1029
|
+
* pair not yet granted is ASKED for (see {@link ConnectorLicence}). */
|
|
823
1030
|
private trySplice(
|
|
824
1031
|
l: OutItem,
|
|
825
1032
|
r: OutItem,
|
|
826
|
-
|
|
1033
|
+
key: string,
|
|
1034
|
+
licence: ConnectorLicence,
|
|
827
1035
|
outsByEnd: Map<number, OutItem[]>,
|
|
828
1036
|
): Rule<GItem> | null {
|
|
829
1037
|
if (!l.rec || !r.rec || r.i < l.j) return null;
|
|
830
1038
|
if (!this.gapRecognised(l.j, r.i, outsByEnd)) return null;
|
|
1039
|
+
const link = licence.granted.get(key);
|
|
1040
|
+
if (link === undefined) licence.asked.add(key);
|
|
1041
|
+
if (!link) return null;
|
|
831
1042
|
return {
|
|
832
1043
|
premises: [l, r],
|
|
833
1044
|
conclusion: {
|
|
@@ -847,11 +1058,15 @@ export class GraphSearch {
|
|
|
847
1058
|
* emit its substitute voice directly. */
|
|
848
1059
|
private *formRules(
|
|
849
1060
|
it: Extract<GItem, { kind: "form" }>,
|
|
850
|
-
|
|
1061
|
+
concepts: ConceptLicence,
|
|
851
1062
|
substitutions: ReadonlyMap<number, Uint8Array> | undefined,
|
|
852
1063
|
nodeBytes: (n: number) => Uint8Array,
|
|
853
|
-
queryLen: number,
|
|
854
1064
|
): Iterable<Rule<GItem>> {
|
|
1065
|
+
// An asking form reached the agenda's front: the hop it stands for is due.
|
|
1066
|
+
if (it.ask) {
|
|
1067
|
+
concepts.asked.add(it.node);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
855
1070
|
// Articulation: emit voice bytes at the recognised span; the hop/concept/
|
|
856
1071
|
// emit chain is suppressed — the form contributes only its substitute.
|
|
857
1072
|
if (substitutions) {
|
|
@@ -886,41 +1101,17 @@ export class GraphSearch {
|
|
|
886
1101
|
// guard then dead-ends it) with no way to reach the forward edge.
|
|
887
1102
|
// Forking offers every continuation as its own rule so the one that
|
|
888
1103
|
// genuinely advances (not a duplicate) is still reachable.
|
|
889
|
-
// A CHAIN HOP OFFERS ONLY WHAT THE QUESTION CAN PAY FOR.
|
|
890
|
-
//
|
|
891
|
-
// `hubBound` = √N is the READ cap — every read here stays inside it — but
|
|
892
|
-
// it is not an exploration bound: measured, a hub of degree 1083 sits
|
|
893
|
-
// BELOW √N = 1559, so a hop offered all 1083 continuations, the chart grew
|
|
894
|
-
// to 3113 outs for a two-word question, and since every out with an
|
|
895
|
-
// uncovered tail probes its tail's prefixes (measured: 16 885 canonical
|
|
896
|
-
// probes = 87% of that query's work, and its 270 MB peak / 256 MB OOM),
|
|
897
|
-
// the cost came from OFFERING rather than from reading.
|
|
898
1104
|
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
// MEASURED in the regime where it used to bite (`hubBound = ceil(√N)`
|
|
911
|
-
// GREATER than the hub's degree — reached in a fixture by choosing the
|
|
912
|
-
// degree below √N, so the trained store is not needed): with the cap the
|
|
913
|
-
// offer was 8/9/9 continuations at degrees 35/70/120; without it, 52/84/120
|
|
914
|
-
// — and the WORK is LINEAR in the degree, not quadratic: pushes 262/296/332,
|
|
915
|
-
// perceptions 530/592/757, while the PEAK is identical with and without the
|
|
916
|
-
// cap (218/415/689 MB against 215/410/662) because it is set by the store,
|
|
917
|
-
// not by the fan-out. What made this hop expensive was never the fan-out
|
|
918
|
-
// breadth: it was the per-offer work, two duplicate/oversized computations
|
|
919
|
-
// since removed (the per-offset canonical scan, and the tail scan now
|
|
920
|
-
// restricted to the fold's boundaries).
|
|
921
|
-
//
|
|
922
|
-
// The residual, stated: the trained store's hub (degree 1 083) is an
|
|
923
|
-
// EXTRAPOLATION from this linear shape, not a measurement.
|
|
1105
|
+
// THE OFFER IS THE CORPUS'S OWN STRUCTURE, and the search pays for it.
|
|
1106
|
+
// In the closure law's terms (derivation.ts) a hop is a MOVE — it reaches
|
|
1107
|
+
// structure the derivation has not stood on — so it is admitted on that
|
|
1108
|
+
// ground and priced one STEP; the A*LD search, not a count, decides how
|
|
1109
|
+
// many are worth taking. `hubBound` is the READ cap (invariant 5) and
|
|
1110
|
+
// nothing else bounds the offer: a cap on what a hop may OFFER was tried
|
|
1111
|
+
// and removed as a short-circuit (trap 13). Measured with the hub's
|
|
1112
|
+
// degree below √N: the work is LINEAR in the degree (pushes 262/296/332 at
|
|
1113
|
+
// degrees 35/70/120) and the peak is set by the store, not the fan-out
|
|
1114
|
+
// (test/112 pins the non-quadratic shape).
|
|
924
1115
|
const nx = this.store.nextFirst(it.node, this.hubBound());
|
|
925
1116
|
// Count what is OFFERED, not what was read: the evidence-preferred
|
|
926
1117
|
// continuation is yielded too, even when it lies outside the cap.
|
|
@@ -1071,8 +1262,24 @@ export class GraphSearch {
|
|
|
1071
1262
|
// Recognised but edge-less: borrow a concept (halo) sibling's edge. No
|
|
1072
1263
|
// edge and no concept means the form leads nowhere — it yields no rule, so
|
|
1073
1264
|
// a query of only such forms produces no derivation, and think is silent.
|
|
1074
|
-
|
|
1075
|
-
|
|
1265
|
+
// An offered form not yet granted yields the hop's ASKING form at the
|
|
1266
|
+
// hop's own cost and span (see {@link Licence}).
|
|
1267
|
+
if (!concepts.offered.has(it.node)) return;
|
|
1268
|
+
const target = concepts.granted.get(it.node);
|
|
1269
|
+
if (target === undefined) {
|
|
1270
|
+
yield {
|
|
1271
|
+
premises: [it],
|
|
1272
|
+
conclusion: {
|
|
1273
|
+
kind: "form",
|
|
1274
|
+
i: it.i,
|
|
1275
|
+
j: it.j,
|
|
1276
|
+
node: it.node,
|
|
1277
|
+
via: true,
|
|
1278
|
+
ask: true,
|
|
1279
|
+
},
|
|
1280
|
+
cost: CONCEPT,
|
|
1281
|
+
};
|
|
1282
|
+
} else if (target !== null) {
|
|
1076
1283
|
yield {
|
|
1077
1284
|
premises: [it],
|
|
1078
1285
|
conclusion: {
|
|
@@ -1211,7 +1418,7 @@ export class GraphSearch {
|
|
|
1211
1418
|
leaves: rec.leaves,
|
|
1212
1419
|
splits: rec.splits,
|
|
1213
1420
|
},
|
|
1214
|
-
|
|
1421
|
+
noLicence(),
|
|
1215
1422
|
undefined,
|
|
1216
1423
|
undefined,
|
|
1217
1424
|
undefined,
|
|
@@ -1245,6 +1452,14 @@ export class GraphSearch {
|
|
|
1245
1452
|
* outs of a long query re-cover each distinct node at most once); reset at the
|
|
1246
1453
|
* top of {@link cover}. */
|
|
1247
1454
|
private recompleteMemo = new Map<number, Uint8Array | null>();
|
|
1455
|
+
/** {@link entityProposals}, per fact BYTES — a pure function of them while
|
|
1456
|
+
* the store is read-only (one response), so a fact the chart reaches as
|
|
1457
|
+
* several items (cover/fix variants, nested completions) is scanned once.
|
|
1458
|
+
* Reset at the top of {@link cover}, like {@link recompleteMemo}. */
|
|
1459
|
+
private entityMemo = new Map<string, EntityProposals>();
|
|
1460
|
+
/** The joins licensed under one cover's {@link Licence}, kept across its
|
|
1461
|
+
* re-covers (see {@link solve}). */
|
|
1462
|
+
private readonly joinsKept = new WeakMap<ConceptLicence, JoinLicense>();
|
|
1248
1463
|
/** The derivation sink of the TOP cover, threaded into every nested
|
|
1249
1464
|
* completion so a produced form's own recompositions are reported in the
|
|
1250
1465
|
* same trace instead of vanishing after the first layer. Undefined when
|
|
@@ -1260,6 +1475,57 @@ export class GraphSearch {
|
|
|
1260
1475
|
* open — the invariant a reader needs to check the guard. */
|
|
1261
1476
|
private recompleteOpen = new Set<number>();
|
|
1262
1477
|
|
|
1478
|
+
/** The ENTITIES a produced fact's bytes contain that lead somewhere — the
|
|
1479
|
+
* candidates a join may travel through. The forms the fact CONTAINS, by the
|
|
1480
|
+
* same recogniser the query went through (so the evidence standard is the
|
|
1481
|
+
* query's), plus — because the recognition of a STORED WHOLE returns the
|
|
1482
|
+
* whole and stops (measured: one site, the fact's own node, for `The
|
|
1483
|
+
* director of Eva is Gustaf Molander.`) — the longest canonically-resolving
|
|
1484
|
+
* form at each offset. A byte atom is never a subject. The scan runs only
|
|
1485
|
+
* for a FORM (≥ W: per letter it measured 20-26 s in test/99), and each probe
|
|
1486
|
+
* is a `canonResolve` whose exact tier decides a miss by its segment probe,
|
|
1487
|
+
* without a fold. The admission predicate is the store's `leadsSomewhere`
|
|
1488
|
+
* (edge or halo); the host lends its memoised form when it can.
|
|
1489
|
+
*
|
|
1490
|
+
* The SOURCE of each proposal travels with it, so a refusal names which path
|
|
1491
|
+
* proposed the candidate. Memoised per fact bytes ({@link entityMemo}). */
|
|
1492
|
+
private entityProposals(factBytes: Uint8Array): EntityProposals {
|
|
1493
|
+
const memoKey = latin1(factBytes);
|
|
1494
|
+
const hit = this.entityMemo.get(memoKey);
|
|
1495
|
+
if (hit !== undefined) return hit;
|
|
1496
|
+
const leads = (id: number): boolean =>
|
|
1497
|
+
this.host.leadsSomewhere !== undefined
|
|
1498
|
+
? this.host.leadsSomewhere(id)
|
|
1499
|
+
: this.store.leadsSomewhere(id);
|
|
1500
|
+
const sites = this.host.recogniseSpan?.(factBytes)?.sites ?? [];
|
|
1501
|
+
const proposed = new Map<number, Uint8Array>();
|
|
1502
|
+
const source = new Map<number, string>();
|
|
1503
|
+
for (const s of sites) {
|
|
1504
|
+
if (s.payload >= 0 && leads(s.payload)) {
|
|
1505
|
+
proposed.set(s.payload, this.store.bytesPrefix(s.payload, ALL));
|
|
1506
|
+
source.set(s.payload, "recognised site");
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
const W = this.maxGroup;
|
|
1510
|
+
if (this.host.canonResolve !== undefined && factBytes.length >= W) {
|
|
1511
|
+
const canon = this.host.canonResolve.bind(this.host);
|
|
1512
|
+
for (let start = 0; start < factBytes.length; start++) {
|
|
1513
|
+
for (let end = factBytes.length; end - start >= W; end--) {
|
|
1514
|
+
const id = canon(factBytes.subarray(start, end));
|
|
1515
|
+
if (id === null) continue;
|
|
1516
|
+
if (leads(id)) {
|
|
1517
|
+
proposed.set(id, this.store.bytesPrefix(id, ALL));
|
|
1518
|
+
source.set(id, "canonical fold");
|
|
1519
|
+
}
|
|
1520
|
+
break; // the longest form at this offset wins
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const out = { proposed, source, sites };
|
|
1525
|
+
this.entityMemo.set(memoKey, out);
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1263
1529
|
/** DERIVE-THROUGH — a RULE this module's DeductionSystem was missing. The
|
|
1264
1530
|
* A*LD library is untouched: this is one more `premises → conclusion + cost`
|
|
1265
1531
|
* rule in the system {@link buildSearch} hands to {@link lightestDerivation},
|
|
@@ -1274,7 +1540,10 @@ export class GraphSearch {
|
|
|
1274
1540
|
* fact reached through it. On the ladder it is one STEP: a direct edge,
|
|
1275
1541
|
* exactly as following a literal continuation is. Deterministic and
|
|
1276
1542
|
* point-probed (`resolve` + `nextFirst`, no scan), so it adds no read that
|
|
1277
|
-
* grows with the corpus.
|
|
1543
|
+
* grows with the corpus. Asked ONCE per fact a lightest derivation stood on
|
|
1544
|
+
* (the join license, {@link solve}) — never per fact the exploration merely
|
|
1545
|
+
* reached — so the number of facts it prices is the answer's, not the
|
|
1546
|
+
* corpus's. The move is visible in the rationale as its own act
|
|
1278
1547
|
* (`classifyMove` reports `derive-through`), distinct from the
|
|
1279
1548
|
* byte-concatenating `fuse`/`splice` — and named `derive-through` rather than
|
|
1280
1549
|
* `join` so it cannot be read as the confluence mechanism's `Provenance`. */
|
|
@@ -1282,13 +1551,14 @@ export class GraphSearch {
|
|
|
1282
1551
|
fact: OutItem,
|
|
1283
1552
|
queryBytes: Uint8Array,
|
|
1284
1553
|
queryLen: number,
|
|
1554
|
+
queryNodes: () => ReadonlySet<number>,
|
|
1285
1555
|
): Iterable<Rule<GItem>> {
|
|
1286
1556
|
if (!this.host.recogniseSpan) return;
|
|
1287
1557
|
const tail = queryBytes.subarray(fact.j, queryLen);
|
|
1288
1558
|
if (tail.length === 0) return;
|
|
1289
|
-
// Report ONLY the invocations that could have joined:
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1559
|
+
// Report ONLY the invocations that could have joined: a derivation stands on
|
|
1560
|
+
// node-bearing outs of every width, including the one-byte outs the cover
|
|
1561
|
+
// bridges with — measured, 68 refusals for a single
|
|
1292
1562
|
// 3-relation query, all of them letters. A form shorter than one window is
|
|
1293
1563
|
// not a fact a join could travel through, so it is not a refusal worth
|
|
1294
1564
|
// reporting; W is the same line the rest of the mind draws between a chance
|
|
@@ -1298,69 +1568,24 @@ export class GraphSearch {
|
|
|
1298
1568
|
// same recogniser the query went through, so the evidence standard is the
|
|
1299
1569
|
// query's. A byte atom is never a subject; the fact's own node is the span
|
|
1300
1570
|
// itself, not an entity inside it. The admission predicate has ONE
|
|
1301
|
-
// definition —
|
|
1302
|
-
//
|
|
1303
|
-
// and a bare host
|
|
1571
|
+
// definition — the store's `leadsSomewhere` (edge or halo); the host LENDS
|
|
1572
|
+
// its memoised form when it can (Mind does, with the response-scoped struct
|
|
1573
|
+
// cache), and a bare host asks the store directly, so the search stays
|
|
1304
1574
|
// host-based.
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
? this.host.leadsSomewhere(id)
|
|
1309
|
-
: this.store.hasNext(id) || this.store.hasHalo(id);
|
|
1575
|
+
const { proposed, source, sites: factSites } = this.entityProposals(
|
|
1576
|
+
fact.bytes,
|
|
1577
|
+
);
|
|
1310
1578
|
// THE QUERY'S OWN SUBJECT, CANONICALLY. The filter used raw bytes and the
|
|
1311
1579
|
// store's nodes are canonical, so `Eiffel Tower country` in the query did
|
|
1312
1580
|
// not match the deposited `eiffel tower country` — measured, that is the
|
|
1313
|
-
// trap's wrong answer.
|
|
1314
|
-
//
|
|
1315
|
-
//
|
|
1316
|
-
//
|
|
1317
|
-
// a second offset scan. `canonicalQueryNodes` re-derived, per byte offset,
|
|
1318
|
-
// what `recognise` had already resolved once per query (its memo is keyed by
|
|
1319
|
-
// content), and that scan was the largest single cost the DIANOT join added:
|
|
1320
|
-
// measured against the pre-change tree, the same fixture and the same test
|
|
1321
|
-
// were 21 s slower with the scan than without it. A recognised site IS a
|
|
1322
|
-
// canonical node of the query that can lead somewhere, which is exactly the
|
|
1323
|
-
// set this filter wants, and it costs nothing to read.
|
|
1324
|
-
const queryNodes = new Set<number>(
|
|
1325
|
-
(this.host.recogniseSpan?.(queryBytes)?.sites ?? []).map((s) =>
|
|
1326
|
-
s.payload
|
|
1327
|
-
),
|
|
1328
|
-
);
|
|
1329
|
-
// TWO SOURCES, ONE ADMISSION. The recognition of a STORED WHOLE returns the
|
|
1330
|
-
// whole and stops — measured: for `The director of Eva is Gustaf Molander.`
|
|
1331
|
-
// it yields exactly ONE site, the fact's own node — so the entity a join
|
|
1332
|
-
// exists for is never proposed. The canonical fold is the second source,
|
|
1333
|
-
// and the scan runs only for a FORM (≥ W: a one-byte out is not something to
|
|
1334
|
-
// join through, and running it per letter measured 20-26 s in test/99).
|
|
1335
|
-
const W = this.maxGroup;
|
|
1336
|
-
const proposed = new Map<number, Uint8Array>();
|
|
1337
|
-
// The SOURCE of each proposal travels with it: a refusal that names only the
|
|
1338
|
-
// bytes leaves the next reader guessing which path proposed them — three
|
|
1339
|
-
// attempts at the chained join were spent fixing paths that never produced
|
|
1340
|
-
// the offending candidate.
|
|
1341
|
-
const source = new Map<number, string>();
|
|
1342
|
-
for (const s of factRec.sites) {
|
|
1343
|
-
if (s.payload >= 0 && leads(s.payload)) {
|
|
1344
|
-
proposed.set(s.payload, this.store.bytesPrefix(s.payload, ALL));
|
|
1345
|
-
source.set(s.payload, "recognised site");
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
if (this.host.canonResolve !== undefined && fact.bytes.length >= W) {
|
|
1349
|
-
const canon = this.host.canonResolve.bind(this.host);
|
|
1350
|
-
for (let start = 0; start < fact.bytes.length; start++) {
|
|
1351
|
-
for (let end = fact.bytes.length; end - start >= W; end--) {
|
|
1352
|
-
const id = canon(fact.bytes.subarray(start, end));
|
|
1353
|
-
if (id === null) continue;
|
|
1354
|
-
if (leads(id)) {
|
|
1355
|
-
proposed.set(id, this.store.bytesPrefix(id, ALL));
|
|
1356
|
-
source.set(id, "canonical fold");
|
|
1357
|
-
}
|
|
1358
|
-
break; // the longest form at this offset wins
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1581
|
+
// trap's wrong answer. Read from the recognition the response already
|
|
1582
|
+
// computed (`queryNodes`, once per solve), never from a second offset scan:
|
|
1583
|
+
// a per-offset `canonicalQueryNodes` re-derived what `recognise` had
|
|
1584
|
+
// resolved once, and was the largest single cost the join first added.
|
|
1362
1585
|
const leading = [...proposed]
|
|
1363
|
-
.filter(([payload]) =>
|
|
1586
|
+
.filter(([payload]) =>
|
|
1587
|
+
payload !== fact.node && !queryNodes().has(payload)
|
|
1588
|
+
)
|
|
1364
1589
|
.map(([payload, bytes]) => ({ payload, bytes }));
|
|
1365
1590
|
// …then prefer the entity the query did NOT name, and the MAXIMAL one. The
|
|
1366
1591
|
// join exists to reach the subject the query never wrote, so:
|
|
@@ -1378,13 +1603,13 @@ export class GraphSearch {
|
|
|
1378
1603
|
// Report WHAT the recognition returned, not just that nothing led: the
|
|
1379
1604
|
// count and the first few site texts are the difference between "the
|
|
1380
1605
|
// fact was not recognised" and "it was recognised but nothing led".
|
|
1381
|
-
const seen =
|
|
1606
|
+
const seen = factSites.slice(0, 3).map((s) =>
|
|
1382
1607
|
this.store.bytesPrefix(s.payload, ALL)
|
|
1383
1608
|
);
|
|
1384
1609
|
this.host.reportSearch?.(
|
|
1385
1610
|
"deriveThroughMiss",
|
|
1386
1611
|
[fact.bytes, tail, ...seen],
|
|
1387
|
-
`no entity inside the fact leads anywhere — ${
|
|
1612
|
+
`no entity inside the fact leads anywhere — ${factSites.length} site(s) recognised inside it`,
|
|
1388
1613
|
);
|
|
1389
1614
|
}
|
|
1390
1615
|
}
|
|
@@ -1496,8 +1721,8 @@ export class GraphSearch {
|
|
|
1496
1721
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
|
1497
1722
|
* learnt connector (the in-search bridge), splitting (at a sub-leaf form
|
|
1498
1723
|
* boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
|
|
1499
|
-
* finalised out, and — for a
|
|
1500
|
-
* with the query's tail ({@link deriveThrough}). */
|
|
1724
|
+
* finalised out, and — for a LICENSED fact — JOINING the entity it contains
|
|
1725
|
+
* with the query's tail ({@link deriveThrough}, granted in {@link solve}). */
|
|
1501
1726
|
private *outRules(
|
|
1502
1727
|
it: OutItem,
|
|
1503
1728
|
ctx: {
|
|
@@ -1511,16 +1736,16 @@ export class GraphSearch {
|
|
|
1511
1736
|
coverableByStart: Map<number, OutItem[]>;
|
|
1512
1737
|
findLeafU: (b: Uint8Array) => number | undefined;
|
|
1513
1738
|
findBranchU: (k: number[]) => number | undefined;
|
|
1514
|
-
linksByLeft
|
|
1515
|
-
linksByRight
|
|
1516
|
-
|
|
1517
|
-
|
|
1739
|
+
linksByLeft: ReadonlyMap<number, Array<[number, string]>>;
|
|
1740
|
+
linksByRight: ReadonlyMap<number, Array<[number, string]>>;
|
|
1741
|
+
connectors?: ConnectorLicence;
|
|
1742
|
+
joins: JoinLicense;
|
|
1518
1743
|
},
|
|
1519
1744
|
): Iterable<Rule<GItem>> {
|
|
1520
1745
|
const { splits, coversDone, outsByStart, outsByEnd, coverableByStart } =
|
|
1521
1746
|
ctx;
|
|
1522
1747
|
const outsByNode = ctx.outsByNode;
|
|
1523
|
-
const byRight = ctx.linksByRight
|
|
1748
|
+
const byRight = ctx.linksByRight;
|
|
1524
1749
|
pushInto(outsByStart, it.i, it);
|
|
1525
1750
|
pushInto(outsByEnd, it.j, it);
|
|
1526
1751
|
if (it.rec && it.node !== undefined) pushInto(outsByNode, it.node, it);
|
|
@@ -1536,23 +1761,25 @@ export class GraphSearch {
|
|
|
1536
1761
|
// Points 2 & 5) — but NEVER across the asker's own unrecognised separator (a
|
|
1537
1762
|
// space, comma), so "ice fire" stays "cold hot", never "cold or hot".
|
|
1538
1763
|
//
|
|
1539
|
-
// Cost stays bounded by iterating only the FEW
|
|
1764
|
+
// Cost stays bounded by iterating only the FEW offered connector targets of
|
|
1540
1765
|
// this out's node (links are selective and, for the N-ary case, keyed first→
|
|
1541
1766
|
// later — O(parts), not O(parts²)), and matching them against finalised outs
|
|
1542
|
-
// by node id, rather than scanning every position pair.
|
|
1767
|
+
// by node id, rather than scanning every position pair. A pair is bridged
|
|
1768
|
+
// only once its two premises have met here ({@link ConnectorLicence}).
|
|
1543
1769
|
const byLeft = ctx.linksByLeft;
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1770
|
+
const licence = ctx.connectors;
|
|
1771
|
+
if (licence && it.rec && it.node !== undefined) {
|
|
1772
|
+
// L = this out, R = a later out whose node is an offered target.
|
|
1773
|
+
for (const [rNode, key] of byLeft.get(it.node) ?? []) {
|
|
1547
1774
|
for (const r of outsByNode.get(rNode) ?? []) {
|
|
1548
|
-
const rule = this.trySplice(it, r,
|
|
1775
|
+
const rule = this.trySplice(it, r, key, licence, outsByEnd);
|
|
1549
1776
|
if (rule) yield rule;
|
|
1550
1777
|
}
|
|
1551
1778
|
}
|
|
1552
|
-
// R = this out, L = an earlier out whose node has
|
|
1553
|
-
for (const [lNode,
|
|
1779
|
+
// R = this out, L = an earlier out whose node has an offered target here.
|
|
1780
|
+
for (const [lNode, key] of byRight.get(it.node) ?? []) {
|
|
1554
1781
|
for (const l of outsByNode.get(lNode) ?? []) {
|
|
1555
|
-
const rule = this.trySplice(l, it,
|
|
1782
|
+
const rule = this.trySplice(l, it, key, licence, outsByEnd);
|
|
1556
1783
|
if (rule) yield rule;
|
|
1557
1784
|
}
|
|
1558
1785
|
}
|
|
@@ -1615,10 +1842,11 @@ export class GraphSearch {
|
|
|
1615
1842
|
// continuation is the derived answer — a genuine relational join, distinct
|
|
1616
1843
|
// from the confluence mechanism's `join` PROVENANCE — and not the
|
|
1617
1844
|
// juxtaposition the cover produces when the intermediate key IS named.
|
|
1618
|
-
//
|
|
1619
|
-
// the
|
|
1620
|
-
|
|
1621
|
-
|
|
1845
|
+
// On the ladder and memoised by {@link key} like every rule, but fired only
|
|
1846
|
+
// for a fact the solve has LICENSED (a fact a lightest derivation stood on —
|
|
1847
|
+
// see {@link solve}), from the joins computed for it once.
|
|
1848
|
+
for (const g of ctx.joins.get(chartKey(it)) ?? []) {
|
|
1849
|
+
yield { premises: [it], conclusion: g.conclusion, cost: g.cost };
|
|
1622
1850
|
}
|
|
1623
1851
|
}
|
|
1624
1852
|
|