@hviana/sema 0.6.0 → 0.7.2
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/.github/workflows/release.yml +80 -0
- package/AGENTS.md +53 -9
- package/HOW_IT_WORKS.md +17 -16
- package/dist/src/meter.d.ts +14 -4
- package/dist/src/meter.js +27 -3
- package/dist/src/mind/attention.js +22 -20
- package/dist/src/mind/graph-search.d.ts +43 -9
- package/dist/src/mind/graph-search.js +82 -15
- package/dist/src/mind/junction.d.ts +13 -0
- package/dist/src/mind/junction.js +26 -1
- package/dist/src/mind/mechanisms/cover.js +23 -2
- package/dist/src/mind/mechanisms/prefix-completion.d.ts +2 -1
- package/dist/src/mind/mechanisms/prefix-completion.js +40 -20
- package/dist/src/mind/mechanisms/recall.js +8 -4
- package/dist/src/mind/pipeline-mechanism.d.ts +0 -24
- package/dist/src/mind/pipeline-mechanism.js +13 -36
- package/dist/src/mind/pipeline.d.ts +24 -0
- package/dist/src/mind/pipeline.js +71 -5
- package/dist/src/mind/recognition.d.ts +15 -1
- package/dist/src/mind/recognition.js +15 -1
- package/dist/src/mind/resonance.js +54 -12
- package/dist/src/store.js +22 -1
- package/jsr.json +1 -1
- package/package.json +7 -2
- package/src/meter.ts +27 -4
- package/src/mind/attention.ts +22 -19
- package/src/mind/graph-search.ts +93 -16
- package/src/mind/junction.ts +25 -1
- package/src/mind/mechanisms/cover.ts +23 -4
- package/src/mind/mechanisms/prefix-completion.ts +40 -20
- package/src/mind/mechanisms/recall.ts +8 -4
- package/src/mind/pipeline-mechanism.ts +13 -42
- package/src/mind/pipeline.ts +106 -5
- package/src/mind/recognition.ts +19 -2
- package/src/mind/resonance.ts +84 -49
- package/src/store.ts +21 -1
- package/test/89-completion-recursion.test.mjs +230 -0
- package/test/90-connector-read-cap.test.mjs +130 -0
- package/test/91-branch-bytes-cache.test.mjs +152 -0
- package/test/93-regime-prediction.test.mjs +148 -0
- package/test/94-cross-region-budget.test.mjs +67 -0
- package/test/95-wide-resonance-removed.test.mjs +109 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
version:
|
|
9
|
+
description: version to release, e.g. 0.6.1
|
|
10
|
+
type: string
|
|
11
|
+
required: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
# Trusted publishing (OIDC): GitHub mints a short-lived identity token that
|
|
15
|
+
# npm and JSR both exchange against the trusted publisher on the package.
|
|
16
|
+
# No registry token is stored anywhere.
|
|
17
|
+
id-token: write
|
|
18
|
+
# The release path bumps package.json + jsr.json, commits, and pushes the tag.
|
|
19
|
+
contents: write
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
publish:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: bump version and tag
|
|
28
|
+
if: github.event_name == 'workflow_dispatch'
|
|
29
|
+
env:
|
|
30
|
+
VERSION: ${{ inputs.version }}
|
|
31
|
+
run: |
|
|
32
|
+
git config user.name "github-actions[bot]"
|
|
33
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
34
|
+
current="$(node -p "require('./package.json').version")"
|
|
35
|
+
if [ "$current" != "$VERSION" ]; then
|
|
36
|
+
npm version "$VERSION" --no-git-tag-version
|
|
37
|
+
node -e 'const fs=require("fs");const p=JSON.parse(fs.readFileSync("jsr.json","utf8"));p.version=process.env.VERSION;fs.writeFileSync("jsr.json",JSON.stringify(p,null,2)+"\n")'
|
|
38
|
+
git add package.json jsr.json
|
|
39
|
+
git commit -m "$VERSION"
|
|
40
|
+
git tag -a "v$VERSION" -m "v$VERSION"
|
|
41
|
+
git push origin HEAD --follow-tags
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# The two manifests must not drift: a dual npm+JSR publish ships the SAME
|
|
45
|
+
# version to both, so a mismatch here would tag one and ship another.
|
|
46
|
+
- name: confirm package.json and jsr.json agree on version
|
|
47
|
+
run: |
|
|
48
|
+
pkg="$(node -p "require('./package.json').version")"
|
|
49
|
+
jsr="$(node -p "require('./jsr.json').version")"
|
|
50
|
+
if [ "$pkg" != "$jsr" ]; then
|
|
51
|
+
echo "package.json version $pkg != jsr.json version $jsr" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
echo "publishing version $pkg"
|
|
55
|
+
|
|
56
|
+
- uses: actions/setup-node@v4
|
|
57
|
+
with:
|
|
58
|
+
node-version: "24"
|
|
59
|
+
registry-url: https://registry.npmjs.org
|
|
60
|
+
|
|
61
|
+
- name: install
|
|
62
|
+
run: npm install
|
|
63
|
+
|
|
64
|
+
# Trusted publishing (OIDC) needs npm >= 11.5.1; Node 24 bundles an older npm.
|
|
65
|
+
- name: update npm for trusted publishing
|
|
66
|
+
run: npm install -g npm@latest
|
|
67
|
+
|
|
68
|
+
# The same run publishes: a tag pushed with GITHUB_TOKEN does NOT
|
|
69
|
+
# re-trigger workflows, so the publish cannot depend on a second run.
|
|
70
|
+
# prepublishOnly (npm test) verifies and builds dist/ before packing.
|
|
71
|
+
# dist/ is gitignored (so it stays off GitHub and JSR) but ships to npm
|
|
72
|
+
# because .npmignore deliberately omits the `dist/` entry.
|
|
73
|
+
- name: publish to npm
|
|
74
|
+
run: npm publish --access public
|
|
75
|
+
|
|
76
|
+
# JSR publishes the SOURCE (jsr.json exports → src/index.ts), not dist/.
|
|
77
|
+
# dist/ is gitignored and JSR honours .gitignore, so it is excluded here
|
|
78
|
+
# with no --allow-dirty needed (the tree is clean after npm's build).
|
|
79
|
+
- name: publish to jsr
|
|
80
|
+
run: npx jsr publish
|
package/AGENTS.md
CHANGED
|
@@ -455,8 +455,7 @@ Asking never writes, which is the only reason per-response memos are sound.
|
|
|
455
455
|
eager fields (recognition, computed spans, guide, the evidence-breadth constant
|
|
456
456
|
`k`) plus **lazily-cached methods** for expensive analyses (`attention()` — the
|
|
457
457
|
consensus climb, `weave()`, `resonance()` — the response's ONE top-k
|
|
458
|
-
content-index read, `
|
|
459
|
-
past-the-top-k mechanism reads, `frames()` — the frame/slot inventory,
|
|
458
|
+
content-index read, `frames()` — the frame/slot inventory,
|
|
460
459
|
`spanShapedOf`/`spanShapedAll`, `queryWindows`, `queryResolved`, `windowsOf`,
|
|
461
460
|
`reachMemo`) — each computed at most once, shared by mechanisms and
|
|
462
461
|
post-grounding stages, and never computed if nobody asks. The async ones are
|
|
@@ -464,10 +463,13 @@ cached **by promise**, so a second caller awaits the first computation rather
|
|
|
464
463
|
than starting another.
|
|
465
464
|
|
|
466
465
|
Mind-level memos (`climbMemo`, `recogniseMemo`, `perceiveMemo`, `canonMemo`,
|
|
467
|
-
`_resolvedSubtrees
|
|
468
|
-
`
|
|
469
|
-
|
|
470
|
-
|
|
466
|
+
`_resolvedSubtrees`) are assigned in `beginResponse()` and nulled in
|
|
467
|
+
`endResponse()` — a new per-response memo must be added to both. A conversation
|
|
468
|
+
supplies its own maps for the first four, so they persist across turns.
|
|
469
|
+
`_edgeChoice` is a Mind field CLEARED in `endResponse()` (not re-created).
|
|
470
|
+
`_gistCache` is a SESSION-lifetime Mind field (32 MB, `mind.ts`) never touched
|
|
471
|
+
by begin/endResponse: a node's bytes are immutable and perception is pure, so a
|
|
472
|
+
cached gist is valid for the store's lifetime.
|
|
471
473
|
|
|
472
474
|
**Which memos a trace bypasses, and why the answer is "almost none".** Only
|
|
473
475
|
`_edgeChoice` (via `guidedNext`) and `sharedReachMemo` are trace-bypassed —
|
|
@@ -546,9 +548,9 @@ answer was chosen, the meter says what it cost. Four contracts:
|
|
|
546
548
|
so two runs are diffable and a work regression is visible without a
|
|
547
549
|
stopwatch. Only `elapsedMs` and the phase millisecond totals are not.
|
|
548
550
|
3. **Phases nest, and carry their own counter deltas.** `think` ⊃ `<mech>.run` ⊃
|
|
549
|
-
`substitutionBridge
|
|
550
|
-
|
|
551
|
-
|
|
551
|
+
`substitutionBridge`. Inclusive, never summed — but each phase reports the
|
|
552
|
+
work done inside it (`PhaseCost.counters`), which is what makes "which phase
|
|
553
|
+
did those byte reads?" answerable at all.
|
|
552
554
|
4. **Count a logical operation once.** A recursive read (`bytesPrefix`
|
|
553
555
|
descending a branch) is charged at the public entry point only — the private
|
|
554
556
|
`_prefix` body is uncharged. Counting the recursion made one read of an
|
|
@@ -608,6 +610,48 @@ corruption via phrase-interior chunks) and the `couldGrow` liveness rule (O(N²)
|
|
|
608
610
|
chart growth). When you fix a subtle bug, leave the constraint behind, not the
|
|
609
611
|
story of the fix.
|
|
610
612
|
|
|
613
|
+
### 2.17 Saturation — every walk decides, none drifts
|
|
614
|
+
|
|
615
|
+
Saturation is a first-class control, not a secondary nicety — and it is TWO
|
|
616
|
+
things, distinguished deliberately:
|
|
617
|
+
|
|
618
|
+
- a CAP — a derived bound (√N per read, √N·W per walk) that exists only to stop
|
|
619
|
+
magic constants (§2.2) — is a SAFETY NET. It bounds the walk when its question
|
|
620
|
+
never decides (a side too common to ever settle). Derived, never tuned; but it
|
|
621
|
+
is not itself a decision.
|
|
622
|
+
- a REAL saturation — a named, derived stop that DECIDES the walk's question and
|
|
623
|
+
terminates the moment it is decided. The cap remains as the backstop; the
|
|
624
|
+
saturation ends the walk. A walk with only a cap drifts to the cap every time;
|
|
625
|
+
a walk with a real saturation stops where the answer is already known.
|
|
626
|
+
|
|
627
|
+
`edgeAncestors` is the model (EXPAND-UNTIL-DECIDED): a reach is consumed either
|
|
628
|
+
as a VOTE (needs `contextsReached` exactly, only while ≤ √N) or as an ABSTENTION
|
|
629
|
+
(`saturated`), so it stops at the FIRST of its five named stops and no consumer
|
|
630
|
+
reads a saturated reach's roots or counts. `pivotInto`'s candidate scan is the
|
|
631
|
+
second: "longest valid wins" is DECIDED at the first valid candidate in
|
|
632
|
+
descending length, so it reads one winner's bytes, never every shorter
|
|
633
|
+
candidate. Saturation is a DECISION about the answer — never a cache, never a
|
|
634
|
+
budget.
|
|
635
|
+
|
|
636
|
+
The junction walk's per-node hub guards are real per-node saturations; its
|
|
637
|
+
`√N·W` budget is the NET, not a saturation. REFUTED (test/16 bridge synthesis,
|
|
638
|
+
test/34 n-ary binding): a "stop once one side's cone is exhausted" early stop is
|
|
639
|
+
WRONG, in both a hub-guarded and a hub-flagged form. The junction test is a BYTE
|
|
640
|
+
containment over the UNION of the two cones, and a junction can be structurally
|
|
641
|
+
reachable from only ONE side — the side whose seed is a FOLD sub-node of the
|
|
642
|
+
container. test/16: "cold or hot" is reached from the window "cold", but the
|
|
643
|
+
3-byte answer "hot" is not a 4-byte window of it, so "hot"'s cone empties after
|
|
644
|
+
one pop while the junction still lies ahead in "cold"'s cone. "One cone
|
|
645
|
+
exhausted" therefore never proves "no junction left", and the budget stays the
|
|
646
|
+
net that backs the per-node saturations.
|
|
647
|
+
|
|
648
|
+
_Follow it:_ when a new walk measures commonality against the corpus, name its
|
|
649
|
+
saturation condition — the answer it may stop producing — beside its read cap.
|
|
650
|
+
No code may traverse the inference uncontrolled from the corpus, nor lack the
|
|
651
|
+
saturation its question admits. A cap without a saturation is a drift, and a
|
|
652
|
+
drifting walk is a bug, not a tuning choice; do not mask it with a cache (§2.12)
|
|
653
|
+
— a cache hides a drift on a warm store, saturation removes it.
|
|
654
|
+
|
|
611
655
|
---
|
|
612
656
|
|
|
613
657
|
## 3. Where things live
|
package/HOW_IT_WORKS.md
CHANGED
|
@@ -4434,16 +4434,19 @@ before the conversion. Derived from the existing bars; never tuned.
|
|
|
4434
4434
|
### 21.5 The refusal path — the substitution bridge, before silence
|
|
4435
4435
|
|
|
4436
4436
|
Everything geometric has now failed. One tier remains, making a **structural**
|
|
4437
|
-
claim about the query that resonance cannot state.
|
|
4438
|
-
**
|
|
4439
|
-
|
|
4440
|
-
threshold. When the query gist has no concept-level match to anything stored, an
|
|
4441
|
-
exhaustive scan would only score more vectors below the bar (profiled at 38–40K
|
|
4442
|
-
vectors scored per refusing query on a 325K-context store, costing 44% of
|
|
4443
|
-
think). Whether the gist ranks _anything_ at concept level is the discriminator
|
|
4444
|
-
— corpus size never was. The list is shared response-wide, so whichever
|
|
4437
|
+
claim about the query that resonance cannot state. Its proposal source is the
|
|
4438
|
+
response's **one top-k read** (`Precomputed.resonance`, §14.5) — the same ranked
|
|
4439
|
+
list recall's earlier tiers already consulted, shared response-wide so whichever
|
|
4445
4440
|
mechanism first-touches it pays once and every later reader is free.
|
|
4446
4441
|
|
|
4442
|
+
It used to be a **wide** list, widened to an exhaustive index scan whenever the
|
|
4443
|
+
top hit cleared the concept threshold. That was removed: the bridge's own
|
|
4444
|
+
candidate cap is `2 · recallQueryK`, so the top-k already IS everything it can
|
|
4445
|
+
consume, and every proposal is byte-verified downstream (§4.3) — an exhaustive
|
|
4446
|
+
scan bought an O(k) need at O(index) cost (profiled: 244K vectors scored per
|
|
4447
|
+
refusing query, ~1.5 s, every answer byte-identical to the top-k read). Test/95
|
|
4448
|
+
pins its absence.
|
|
4449
|
+
|
|
4447
4450
|
#### The substitution bridge
|
|
4448
4451
|
|
|
4449
4452
|
**The gap.** A query phrased through a near-synonym of a trained word ("Name the
|
|
@@ -5288,16 +5291,14 @@ recallByResonance(query, pre):
|
|
|
5288
5291
|
return { bytes: g, accounted: nothing, moves: STEP }
|
|
5289
5292
|
|
|
5290
5293
|
# ── the REFUSAL PATH ─────────────────────────────────────────────
|
|
5291
|
-
# pre.
|
|
5292
|
-
#
|
|
5293
|
-
#
|
|
5294
|
-
#
|
|
5295
|
-
# : hits # the gist ranks nothing at concept
|
|
5296
|
-
# # level, so a wider scan says nothing
|
|
5294
|
+
# pre.resonance() — the response's ONE top-k read, already paid for by
|
|
5295
|
+
# the tiers above. The bridge caps its own candidates at 2·recallQueryK,
|
|
5296
|
+
# so the top-k is exactly the budget it can consume; there is no wider
|
|
5297
|
+
# list, and every proposal is byte-verified below.
|
|
5297
5298
|
|
|
5298
5299
|
# 3b. substitution / identity bridge
|
|
5299
|
-
bridged ≔ substitutionBridge(query, pre.
|
|
5300
|
-
# anchors: rarest query windows → edgeAncestors, plus
|
|
5300
|
+
bridged ≔ substitutionBridge(query, ids(pre.resonance))
|
|
5301
|
+
# anchors: rarest query windows → edgeAncestors, plus those ids
|
|
5301
5302
|
# align byte-for-byte; a mismatch substitutes only under
|
|
5302
5303
|
# CORROBORATION ∧ GRADED IDENTITY ∧ RAW BALANCE
|
|
5303
5304
|
# accept when matched+substituted DOMINATES the query, every
|
package/dist/src/meter.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* PHASES NEST, AND ARE NOT DISJOINT. `think` contains every mechanism
|
|
5
5
|
* phase; a mechanism's `floor` contains whatever shared analysis it
|
|
6
6
|
* first-touched (`attention`, `weave`); `recall.run` contains
|
|
7
|
-
* `substitutionBridge
|
|
7
|
+
* `substitutionBridge`. Read a
|
|
8
8
|
* phase as "wall-clock spent inside this, inclusive" — never sum them and
|
|
9
9
|
* expect the total. `CostReport.elapsedMs` is the only whole.
|
|
10
10
|
*
|
|
@@ -129,6 +129,13 @@ export declare class Meter {
|
|
|
129
129
|
/** Nodes popped by those ascents, against their √N·W budget — the counter
|
|
130
130
|
* that shows whether the walks are deciding early or burning the budget. */
|
|
131
131
|
junctionPops: number;
|
|
132
|
+
/** Ascents that ended by EXHAUSTING the expansion budget rather than by
|
|
133
|
+
* deciding — the walk abstained and the caller silently fell through to a
|
|
134
|
+
* lower tier of the ladder (§2.13: a degradation nothing else reports).
|
|
135
|
+
* It rises the moment a SHARED budget is drained by an earlier walk, which
|
|
136
|
+
* is what makes "this tier answered nothing" distinguishable from "this
|
|
137
|
+
* tier never got to look". */
|
|
138
|
+
junctionBudgetExhausted: number;
|
|
132
139
|
/** Arbitrary byte spans whose distributional company was VSA-bundled from
|
|
133
140
|
* existing episode halos. */
|
|
134
141
|
spanHalos: number;
|
|
@@ -148,9 +155,6 @@ export declare class Meter {
|
|
|
148
155
|
mechanismRuns: number;
|
|
149
156
|
/** Candidates the decider weighed. */
|
|
150
157
|
candidates: number;
|
|
151
|
-
/** Candidates refused before the competition for explaining less than 1/W
|
|
152
|
-
* of the query — the honesty-density floor (see pipeline.ts `consider`). */
|
|
153
|
-
thinRejects: number;
|
|
154
158
|
private readonly _phases;
|
|
155
159
|
private readonly _t0;
|
|
156
160
|
/** Every work counter's current value, by name — the snapshot `time`
|
|
@@ -159,6 +163,12 @@ export declare class Meter {
|
|
|
159
163
|
/** Charge `ms`, one call, and a counter delta to a named phase.
|
|
160
164
|
* Insertion-ordered, so a report reads in execution order. */
|
|
161
165
|
charge(phase: string, ms: number, delta?: Record<string, number>): void;
|
|
166
|
+
/** Time one SYNCHRONOUS phase. The sync/async seam (§2.10) is a real
|
|
167
|
+
* contract — perception, recognition and the graph search are synchronous —
|
|
168
|
+
* so a synchronous layer must not be wrapped in `time`'s promise just to be
|
|
169
|
+
* measured: that would make the profiled path await where the unprofiled
|
|
170
|
+
* one does not, and a meter never changes what a layer computes. */
|
|
171
|
+
timeSync<T>(phase: string, fn: () => T): T;
|
|
162
172
|
/** Time one async phase and attribute the work done inside it. Returns
|
|
163
173
|
* the awaited value untouched — a meter never changes what a layer
|
|
164
174
|
* computes, only what is known about it. */
|
package/dist/src/meter.js
CHANGED
|
@@ -122,6 +122,13 @@ export class Meter {
|
|
|
122
122
|
/** Nodes popped by those ascents, against their √N·W budget — the counter
|
|
123
123
|
* that shows whether the walks are deciding early or burning the budget. */
|
|
124
124
|
junctionPops = 0;
|
|
125
|
+
/** Ascents that ended by EXHAUSTING the expansion budget rather than by
|
|
126
|
+
* deciding — the walk abstained and the caller silently fell through to a
|
|
127
|
+
* lower tier of the ladder (§2.13: a degradation nothing else reports).
|
|
128
|
+
* It rises the moment a SHARED budget is drained by an earlier walk, which
|
|
129
|
+
* is what makes "this tier answered nothing" distinguishable from "this
|
|
130
|
+
* tier never got to look". */
|
|
131
|
+
junctionBudgetExhausted = 0;
|
|
125
132
|
/** Arbitrary byte spans whose distributional company was VSA-bundled from
|
|
126
133
|
* existing episode halos. */
|
|
127
134
|
spanHalos = 0;
|
|
@@ -142,9 +149,6 @@ export class Meter {
|
|
|
142
149
|
mechanismRuns = 0;
|
|
143
150
|
/** Candidates the decider weighed. */
|
|
144
151
|
candidates = 0;
|
|
145
|
-
/** Candidates refused before the competition for explaining less than 1/W
|
|
146
|
-
* of the query — the honesty-density floor (see pipeline.ts `consider`). */
|
|
147
|
-
thinRejects = 0;
|
|
148
152
|
// ── Phases ──────────────────────────────────────────────────────────────
|
|
149
153
|
_phases = new Map();
|
|
150
154
|
_t0 = performance.now();
|
|
@@ -176,6 +180,26 @@ export class Meter {
|
|
|
176
180
|
}
|
|
177
181
|
}
|
|
178
182
|
}
|
|
183
|
+
/** Time one SYNCHRONOUS phase. The sync/async seam (§2.10) is a real
|
|
184
|
+
* contract — perception, recognition and the graph search are synchronous —
|
|
185
|
+
* so a synchronous layer must not be wrapped in `time`'s promise just to be
|
|
186
|
+
* measured: that would make the profiled path await where the unprofiled
|
|
187
|
+
* one does not, and a meter never changes what a layer computes. */
|
|
188
|
+
timeSync(phase, fn) {
|
|
189
|
+
const before = this.snapshot();
|
|
190
|
+
const t = performance.now();
|
|
191
|
+
try {
|
|
192
|
+
return fn();
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
const ms = performance.now() - t;
|
|
196
|
+
const after = this.snapshot();
|
|
197
|
+
const delta = {};
|
|
198
|
+
for (const k of Object.keys(after))
|
|
199
|
+
delta[k] = after[k] - before[k];
|
|
200
|
+
this.charge(phase, ms, delta);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
179
203
|
/** Time one async phase and attribute the work done inside it. Returns
|
|
180
204
|
* the awaited value untouched — a meter never changes what a layer
|
|
181
205
|
* computes, only what is known about it. */
|
|
@@ -12,7 +12,7 @@ import { composeStructuralGist, consensusFloor, dominates, estimatorNoise, } fro
|
|
|
12
12
|
import { foldTree, gistOf, latin1Key, perceive, read, resolve, } from "./primitives.js";
|
|
13
13
|
import { recognise } from "./recognition.js";
|
|
14
14
|
import { leafIdRun } from "./canonical.js";
|
|
15
|
-
import { corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
|
|
15
|
+
import { atomIsHub, corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
|
|
16
16
|
import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, loadJunctionSynonymSides, walkCache, } from "./junction.js";
|
|
17
17
|
import { indexOf } from "../bytes.js";
|
|
18
18
|
import { rItem, rNode, traceDerivation } from "./trace.js";
|
|
@@ -1848,26 +1848,28 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
|
|
|
1848
1848
|
// the same container (or a sub-container of it) twice.
|
|
1849
1849
|
const consumed = new Set();
|
|
1850
1850
|
let probes = 0;
|
|
1851
|
-
//
|
|
1852
|
-
//
|
|
1853
|
-
//
|
|
1854
|
-
//
|
|
1855
|
-
//
|
|
1856
|
-
//
|
|
1851
|
+
// When atoms themselves are hubs (atomIsHub — a single byte reaches ≥ √N
|
|
1852
|
+
// contexts, §2.8's own predicate), the corpus is large enough that the
|
|
1853
|
+
// cross-region junction walks are dominated by the drift through common
|
|
1854
|
+
// content's ancestry. Each of k candidate pairs otherwise spends its own
|
|
1855
|
+
// √N·W budget (profiled: 160,210 junction pops, 31% of think at
|
|
1856
|
+
// N = 325,608), and a cumulative dialogue multiplies bounded work into tens
|
|
1857
|
+
// of seconds. The structural walk is therefore given ONE k·W allowance per
|
|
1858
|
+
// evidence tier, shared across every pair — k pairs × W phrase-scale levels,
|
|
1859
|
+
// the minimal exact check; a pair whose container is not reached within it
|
|
1860
|
+
// falls through to the resonance tier (the ANN proposes what the shallow
|
|
1861
|
+
// walk no longer exhaustively scans, §2.3).
|
|
1857
1862
|
//
|
|
1858
|
-
//
|
|
1859
|
-
//
|
|
1860
|
-
//
|
|
1861
|
-
//
|
|
1862
|
-
//
|
|
1863
|
-
//
|
|
1864
|
-
// too tight below ~10³ contexts
|
|
1865
|
-
//
|
|
1866
|
-
//
|
|
1867
|
-
|
|
1868
|
-
// already makes — pricing the gate on the synthetic corpora.
|
|
1869
|
-
const marketScale = k * ctx.space.maxGroup;
|
|
1870
|
-
const corpusScale = N > marketScale ** 3;
|
|
1863
|
+
// Below atomIsHub the store is small and atoms still discriminate, so the
|
|
1864
|
+
// walks keep exhaustive exact traversal (per-walk √N·W) — the shared budget
|
|
1865
|
+
// would otherwise be smaller than the structures the tests deliberately
|
|
1866
|
+
// build. The gate is the SAME derived predicate the climb already uses for
|
|
1867
|
+
// byte atoms, not a separate corpus-size knob: an earlier `N > (k·W)³` cube
|
|
1868
|
+
// never engaged at real scale (96³ = 884,736 > 325,608), and a "share one
|
|
1869
|
+
// √N·W" experiment was too tight below ~10³ contexts (test/36, test/14) —
|
|
1870
|
+
// both are the same mistake of pricing the gate on corpus size instead of on
|
|
1871
|
+
// the atom-hub scale.
|
|
1872
|
+
const corpusScale = atomIsHub(ctx, N);
|
|
1871
1873
|
const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
1872
1874
|
const synonymBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
1873
1875
|
for (let a = 0; a < cand.length && probes < k; a++) {
|
|
@@ -66,6 +66,13 @@ export type GItem = {
|
|
|
66
66
|
* because subtraction is more "the point" of its query). See
|
|
67
67
|
* {@link liftAnswer}. */
|
|
68
68
|
computed?: boolean;
|
|
69
|
+
/** Set on the out emitted at a chain's GENUINE FIXPOINT — the one span kind a
|
|
70
|
+
* recursive re-cover ({@link GraphSearch.recompleteNode}) may deepen. The
|
|
71
|
+
* re-cover does NOT run here: this marks the span as eligible, and
|
|
72
|
+
* {@link GraphSearch.deepen} runs it afterwards on the spans the lightest
|
|
73
|
+
* derivation actually CHOSE. Part of {@link key}, because it decides
|
|
74
|
+
* whether the span's final bytes may still change. */
|
|
75
|
+
fix?: boolean;
|
|
69
76
|
};
|
|
70
77
|
export declare const STEP = 1;
|
|
71
78
|
export declare const CONCEPT = 10;
|
|
@@ -87,6 +94,10 @@ export interface Seg {
|
|
|
87
94
|
/** See the `computed` field of the "out" {@link GItem} — set only for an
|
|
88
95
|
* extension's derived value, never a genuinely recognised learned form. */
|
|
89
96
|
computed?: boolean;
|
|
97
|
+
/** See the `fix` field of the "out" {@link GItem} — this span ended a chain at
|
|
98
|
+
* a genuine fixpoint, so it is the one span kind a recursive re-cover may
|
|
99
|
+
* still deepen. Consumed by {@link GraphSearch.deepen}. */
|
|
100
|
+
fix?: boolean;
|
|
90
101
|
}
|
|
91
102
|
/** One rule application inside the cover's lightest derivation — the FINEST
|
|
92
103
|
* grain of Sema's core reasoning, one node of the adapted A*LD proof tree. `move`
|
|
@@ -192,6 +203,20 @@ export declare class GraphSearch {
|
|
|
192
203
|
* decomposes, two recomposes, any mix — and stops only when it reaches a node
|
|
193
204
|
* that leads nowhere new, never at an arbitrary count. */
|
|
194
205
|
private solve;
|
|
206
|
+
/** Re-cover the CHOSEN fixpoint spans, in place.
|
|
207
|
+
*
|
|
208
|
+
* Completion is still "cover, recursively" — it just runs on the answer
|
|
209
|
+
* instead of on the exploration. A cover chooses O(segs) spans, so a level
|
|
210
|
+
* pays O(answer) re-covers however densely the corpus interconnects the forms
|
|
211
|
+
* the search passed through on the way. That is the bound
|
|
212
|
+
* {@link recompleteNode}'s contract always claimed and, running per fixpoint
|
|
213
|
+
* REACHED, never had.
|
|
214
|
+
*
|
|
215
|
+
* Deepening cannot change which cover won: the derivation is already final
|
|
216
|
+
* and every span keeps its i..j and its cost. It only replaces a chosen
|
|
217
|
+
* span's bytes with the deeper learnt form they rewrite to — what the
|
|
218
|
+
* recursion was always for. */
|
|
219
|
+
private deepen;
|
|
195
220
|
/** The weighted deduction system the graph exploration solves (the four
|
|
196
221
|
* reductions of adapted A*LD live in {@link lightestDerivation}; this only states the
|
|
197
222
|
* items, axioms, goal, and rules — see {@link GItem} for the item kinds).
|
|
@@ -253,20 +278,29 @@ export declare class GraphSearch {
|
|
|
253
278
|
* ({@link resolve}) — the graph itself gates against re-expanding a contained
|
|
254
279
|
* form ("ice is cold" ⊅→ "ice is cold is cold").
|
|
255
280
|
*
|
|
256
|
-
* Termination is
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
281
|
+
* Termination is STRUCTURAL: a produced node is re-covered once, never inside
|
|
282
|
+
* another re-cover (see the guard below), so one cover pays at most one
|
|
283
|
+
* nested {@link solve} per distinct produced node it actually reaches, and
|
|
284
|
+
* {@link recompleteMemo} collapses a repeat to nothing.
|
|
285
|
+
*
|
|
286
|
+
* This comment used to argue termination from "distinct node ids are finite
|
|
287
|
+
* and each finished completion is memoised". That is a bound of N — the one
|
|
288
|
+
* AGENTS §2.8 forbids — and it was load-bearing, not pedantic: nested, the
|
|
289
|
+
* recursion reached depth 331 and 9.1 GB on an 18.9M-node store for a 2-byte
|
|
290
|
+
* query and did not terminate, which is what killed a 5 h training run at its
|
|
291
|
+
* checkpoint recall. Guard: test/89-completion-recursion.test.mjs. */
|
|
262
292
|
private recompleteNode;
|
|
263
293
|
/** Per-cover memo of each produced node's completion (so the many terminal
|
|
264
294
|
* outs of a long query re-cover each distinct node at most once); reset at the
|
|
265
295
|
* top of {@link cover}. */
|
|
266
296
|
private recompleteMemo;
|
|
267
|
-
/** The
|
|
268
|
-
*
|
|
269
|
-
* (
|
|
297
|
+
/** The node currently being re-completed — the recursion stack, and so also
|
|
298
|
+
* the nesting depth. {@link recompleteNode} refuses to start while it is
|
|
299
|
+
* non-empty (one re-cover per produced node, never one inside another), which
|
|
300
|
+
* is what keeps a query's cost proportional to its answer rather than to the
|
|
301
|
+
* corpus; it therefore holds at most one id. A Set, not a flag, because it
|
|
302
|
+
* states WHICH node is open — the invariant a reader needs to check the
|
|
303
|
+
* guard, and what makes the old cycle-guard reading still hold. */
|
|
270
304
|
private recompleteOpen;
|
|
271
305
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
|
272
306
|
* learnt connector (the in-search bridge), splitting (at a sub-leaf form
|
|
@@ -64,6 +64,7 @@ function readCover(derivation) {
|
|
|
64
64
|
rec: out.rec,
|
|
65
65
|
node: out.node,
|
|
66
66
|
computed: out.computed,
|
|
67
|
+
fix: out.fix,
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
node = node.premises[0];
|
|
@@ -269,9 +270,32 @@ export class GraphSearch {
|
|
|
269
270
|
onDerivation(readDerivation(derivation, substitutions !== undefined));
|
|
270
271
|
}
|
|
271
272
|
return derivation
|
|
272
|
-
? { segs: readCover(derivation), cost: derivation.cost }
|
|
273
|
+
? { segs: this.deepen(readCover(derivation)), cost: derivation.cost }
|
|
273
274
|
: null;
|
|
274
275
|
}
|
|
276
|
+
/** Re-cover the CHOSEN fixpoint spans, in place.
|
|
277
|
+
*
|
|
278
|
+
* Completion is still "cover, recursively" — it just runs on the answer
|
|
279
|
+
* instead of on the exploration. A cover chooses O(segs) spans, so a level
|
|
280
|
+
* pays O(answer) re-covers however densely the corpus interconnects the forms
|
|
281
|
+
* the search passed through on the way. That is the bound
|
|
282
|
+
* {@link recompleteNode}'s contract always claimed and, running per fixpoint
|
|
283
|
+
* REACHED, never had.
|
|
284
|
+
*
|
|
285
|
+
* Deepening cannot change which cover won: the derivation is already final
|
|
286
|
+
* and every span keeps its i..j and its cost. It only replaces a chosen
|
|
287
|
+
* span's bytes with the deeper learnt form they rewrite to — what the
|
|
288
|
+
* recursion was always for. */
|
|
289
|
+
deepen(segs) {
|
|
290
|
+
for (const s of segs) {
|
|
291
|
+
if (!s.fix || s.node === undefined)
|
|
292
|
+
continue;
|
|
293
|
+
const deeper = this.recompleteNode(s.node);
|
|
294
|
+
if (deeper !== null)
|
|
295
|
+
s.bytes = deeper;
|
|
296
|
+
}
|
|
297
|
+
return segs;
|
|
298
|
+
}
|
|
275
299
|
/** The weighted deduction system the graph exploration solves (the four
|
|
276
300
|
* reductions of adapted A*LD live in {@link lightestDerivation}; this only states the
|
|
277
301
|
* items, axioms, goal, and rules — see {@link GItem} for the item kinds).
|
|
@@ -319,7 +343,7 @@ export class GraphSearch {
|
|
|
319
343
|
if (it.kind === "form") {
|
|
320
344
|
return `f${it.i}.${it.j}.${it.node}.${it.via ? 1 : 0}.${it.rcmp ? 1 : 0}`;
|
|
321
345
|
}
|
|
322
|
-
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
346
|
+
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${it.fix ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
323
347
|
},
|
|
324
348
|
*axioms() {
|
|
325
349
|
yield { item: { kind: "cover", p: 0 }, cost: 0 };
|
|
@@ -614,17 +638,29 @@ export class GraphSearch {
|
|
|
614
638
|
// actual end, never per intermediate stop — so its cost tracks the
|
|
615
639
|
// ANSWER's own structure, not how densely the corpus interconnects
|
|
616
640
|
// the nodes passed through on the way there.
|
|
617
|
-
|
|
641
|
+
// MARK the fixpoint; do not re-cover it here. Re-covering at this point
|
|
642
|
+
// pays a full {@link recompleteNode} — a recognition of the node's whole
|
|
643
|
+
// bytes — for every fixpoint the exploration REACHES, and how many it
|
|
644
|
+
// reaches is set by how densely the corpus interconnects the forms passed
|
|
645
|
+
// through. Measured on an 18.9M-node store, a 2-byte query: 12,000
|
|
646
|
+
// re-covers inside ONE cover, folding 95,258 distinct spans, ~2.4 GB and
|
|
647
|
+
// climbing to a V8 fatal. The answer needs a handful.
|
|
648
|
+
//
|
|
649
|
+
// {@link deepen} runs it instead on the spans the lightest derivation
|
|
650
|
+
// CHOSE. The ladder is untouched — this out still costs 0, so a genuine
|
|
651
|
+
// fixpoint still beats any premature stop at the same depth, exactly as
|
|
652
|
+
// the ordering above states.
|
|
618
653
|
yield {
|
|
619
654
|
premises: [it],
|
|
620
655
|
conclusion: {
|
|
621
656
|
kind: "out",
|
|
622
657
|
i: it.i,
|
|
623
658
|
j: it.j,
|
|
624
|
-
bytes:
|
|
659
|
+
bytes: nodeBytes(it.node),
|
|
625
660
|
cover: true,
|
|
626
661
|
rec: true,
|
|
627
662
|
node: it.node,
|
|
663
|
+
fix: true,
|
|
628
664
|
},
|
|
629
665
|
cost: 0,
|
|
630
666
|
};
|
|
@@ -694,20 +730,47 @@ export class GraphSearch {
|
|
|
694
730
|
* ({@link resolve}) — the graph itself gates against re-expanding a contained
|
|
695
731
|
* form ("ice is cold" ⊅→ "ice is cold is cold").
|
|
696
732
|
*
|
|
697
|
-
* Termination is
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
733
|
+
* Termination is STRUCTURAL: a produced node is re-covered once, never inside
|
|
734
|
+
* another re-cover (see the guard below), so one cover pays at most one
|
|
735
|
+
* nested {@link solve} per distinct produced node it actually reaches, and
|
|
736
|
+
* {@link recompleteMemo} collapses a repeat to nothing.
|
|
737
|
+
*
|
|
738
|
+
* This comment used to argue termination from "distinct node ids are finite
|
|
739
|
+
* and each finished completion is memoised". That is a bound of N — the one
|
|
740
|
+
* AGENTS §2.8 forbids — and it was load-bearing, not pedantic: nested, the
|
|
741
|
+
* recursion reached depth 331 and 9.1 GB on an 18.9M-node store for a 2-byte
|
|
742
|
+
* query and did not terminate, which is what killed a 5 h training run at its
|
|
743
|
+
* checkpoint recall. Guard: test/89-completion-recursion.test.mjs. */
|
|
703
744
|
recompleteNode(node) {
|
|
704
745
|
if (!this.host.recogniseSpan)
|
|
705
746
|
return null;
|
|
706
747
|
const memo = this.recompleteMemo;
|
|
707
748
|
if (memo.has(node))
|
|
708
749
|
return memo.get(node) ?? null;
|
|
709
|
-
//
|
|
710
|
-
|
|
750
|
+
// ONE re-cover per produced node — never a re-cover inside a re-cover.
|
|
751
|
+
//
|
|
752
|
+
// Re-covering is how a PRODUCED node's bytes enter the search at all: the
|
|
753
|
+
// cover machinery otherwise only ever sees the QUERY's spans. That is
|
|
754
|
+
// needed once. The alternation of decomposition and recomposition that
|
|
755
|
+
// follows — parts rewriting several times, siblings fusing, a recomposition
|
|
756
|
+
// feeding another — is the main search's own fuse/`rcmp` work, not this
|
|
757
|
+
// recursion's: 15-decomposition-gap §9–§12 all pass with this method
|
|
758
|
+
// disabled outright, and only §6 (the produced composite "p1 p2", whose
|
|
759
|
+
// bytes nothing else brings in) needs it.
|
|
760
|
+
//
|
|
761
|
+
// Nesting it was the defect. Each level is a full {@link solve} with its
|
|
762
|
+
// own agenda and chart, exploring from a node the answer never asked about,
|
|
763
|
+
// so per-query cost tracked how densely the corpus interconnects the forms
|
|
764
|
+
// passed through — the growth AGENTS §2.8 forbids. Measured on an
|
|
765
|
+
// 18.9M-node store: depth 331 and 9.1 GB for a 2-byte query, not
|
|
766
|
+
// terminating; and on the guard corpus every one of 125 nested re-covers
|
|
767
|
+
// was REJECTED by the resolve() gate below, expanding a 70-byte node into a
|
|
768
|
+
// 374-byte concatenation that names nothing. All of it was waste.
|
|
769
|
+
//
|
|
770
|
+
// `recompleteOpen` is that stack, so a non-empty stack means we are already
|
|
771
|
+
// inside one. This subsumes the old cycle guard: a node cannot recurse
|
|
772
|
+
// back into itself when nothing recurses at all.
|
|
773
|
+
if (this.recompleteOpen.size > 0)
|
|
711
774
|
return null;
|
|
712
775
|
// A leaf or single-child node has no parts to recompose; skip before the
|
|
713
776
|
// costly recognition so a plain terminal answer pays nothing.
|
|
@@ -740,9 +803,13 @@ export class GraphSearch {
|
|
|
740
803
|
* outs of a long query re-cover each distinct node at most once); reset at the
|
|
741
804
|
* top of {@link cover}. */
|
|
742
805
|
recompleteMemo = new Map();
|
|
743
|
-
/** The
|
|
744
|
-
*
|
|
745
|
-
* (
|
|
806
|
+
/** The node currently being re-completed — the recursion stack, and so also
|
|
807
|
+
* the nesting depth. {@link recompleteNode} refuses to start while it is
|
|
808
|
+
* non-empty (one re-cover per produced node, never one inside another), which
|
|
809
|
+
* is what keeps a query's cost proportional to its answer rather than to the
|
|
810
|
+
* corpus; it therefore holds at most one id. A Set, not a flag, because it
|
|
811
|
+
* states WHICH node is open — the invariant a reader needs to check the
|
|
812
|
+
* guard, and what makes the old cycle-guard reading still hold. */
|
|
746
813
|
recompleteOpen = new Set();
|
|
747
814
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
|
748
815
|
* learnt connector (the in-search bridge), splitting (at a sub-leaf form
|