@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
|
@@ -12,6 +12,15 @@ discovering bugs:**
|
|
|
12
12
|
leads to accidental bugs, and real-world corpus must not be compromised.
|
|
13
13
|
- Something that happens due to deduplication, a tie-breaking rule, etc., isn't
|
|
14
14
|
a bug—and that’s a subtle point.
|
|
15
|
+
- A change in behavior is not a behavioral regression: A change that shifts a
|
|
16
|
+
response from correct to incorrect is not necessarily a regression. Sometimes,
|
|
17
|
+
there may be many other responses that were incorrect but have become correct.
|
|
18
|
+
Overall, there must be a net gain. Budget adjustments or computational
|
|
19
|
+
optimizations often require this.
|
|
20
|
+
- Often, a pathological search is not necessarily resolved by a constant
|
|
21
|
+
derivative. A constant derivative is typically a type of short-circuit
|
|
22
|
+
breaker; it can cause truncation and is not necessarily a budget-related
|
|
23
|
+
measure. Therefore, it must be used wisely.
|
|
15
24
|
|
|
16
25
|
### 1. `score >= threshold` decides identity
|
|
17
26
|
|
|
@@ -174,8 +183,10 @@ discovering bugs:**
|
|
|
174
183
|
the quantities the machine already has (`leadsSomewhere`, the
|
|
175
184
|
exact-then-canonical identity, `accounted` bytes, the ladder, `hubBound`),
|
|
176
185
|
from which the reach of a gap, the offer of a hop, the depth of a join and the
|
|
177
|
-
scope of a substitution are CONSEQUENCES, not four separate decisions.
|
|
178
|
-
|
|
186
|
+
scope of a substitution are CONSEQUENCES, not four separate decisions. Where
|
|
187
|
+
the repository stands against it — the engine (`closeOver`), which of the four
|
|
188
|
+
follow from the law, and the one that does not, with the reason — is stated in
|
|
189
|
+
`docs/architecture/closure.md`, and nowhere else.
|
|
179
190
|
- **THE STANDARD A CHANGE MUST MEET:** state which consequence it is, and show
|
|
180
191
|
it following from the law. A change that cannot be stated that way is not
|
|
181
192
|
ready.
|
package/docs/harness/gates.md
CHANGED
|
@@ -13,22 +13,24 @@ Guards honest silence, determinism, and every pinned contract. Silence:
|
|
|
13
13
|
unrelated queries ground to nothing (`test/28`, `50`, `56`, `67`, `76`, `84`).
|
|
14
14
|
Determinism: same seed + deposit order + query gives byte-identical answer
|
|
15
15
|
(`test/20`). Every invariant is pinned, the closure law included
|
|
16
|
-
(`test/133`–`
|
|
16
|
+
(`test/133`–`151`). §14–25 (pipeline), §64 (derived thresholds), AGENTS.md §2
|
|
17
17
|
invariants 1–5.
|
|
18
18
|
|
|
19
19
|
## 2 — Work accounting (profiler)
|
|
20
20
|
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
```js
|
|
22
|
+
const mind = new Mind({ profile: true }); // meter attached per response
|
|
23
|
+
await mind.respondText(q); // then read mind.lastCost
|
|
24
|
+
console.log(formatReport(mind.lastCost)); // sumReports() over several
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`
|
|
27
|
+
The public path is the harness (`AGENTS.md` §6); there is no separate bench
|
|
28
|
+
script. Guards without trace: counters exact and diffable between COLD runs;
|
|
29
|
+
phases nest (not disjoint — each phase is charged by its own layer); shared
|
|
30
|
+
analyses charged to themselves, not to the first toucher; millisecond fields are
|
|
31
|
+
non-deterministic hints only. With an `inspectRationale` callback attached,
|
|
32
|
+
recognition idempotence still holds (`test/42`). `src/meter.ts`,
|
|
33
|
+
`docs/architecture/meter.md`, §55, `AGENTS.md` §6.
|
|
32
34
|
|
|
33
35
|
## 3 — Dependency footprint
|
|
34
36
|
|
package/docs/mechanisms/cover.md
CHANGED
|
@@ -15,8 +15,15 @@ consumes them directly; any site whose bytes overlap a computed span is masked
|
|
|
15
15
|
- `formRules` follow continuation edges (`GraphSearch.formRules`): each hop
|
|
16
16
|
costs `STEP` (1). Forks across all continuations up to the hub bound;
|
|
17
17
|
disambiguation is distributional, not heuristic.
|
|
18
|
-
- Edge-less forms may hop via a halo sibling (`conceptHop` / `
|
|
19
|
-
|
|
18
|
+
- Edge-less forms may hop via a halo sibling (`conceptHop` / `offerConcepts`) at
|
|
19
|
+
`CONCEPT` (10), borrowing a synonym's continuation.
|
|
20
|
+
- A span's cheapest completion DOMINATES the rest (`buildSearch`): a form or
|
|
21
|
+
completion of `[i, j)` whose cost has reached that of a completion of `[i, j)`
|
|
22
|
+
already yielded fires no rule (`searchDominated`). Coverage is positional, so
|
|
23
|
+
only the cheapest matters to the goal; the byte rules (fuse, splice, join)
|
|
24
|
+
fire from the completion the search would stand on, never from every
|
|
25
|
+
alternative it reached. It also makes the first hop's stop-here
|
|
26
|
+
(`STEP + CONCEPT`) a real horizon for the chain.
|
|
20
27
|
|
|
21
28
|
## Gate — `leadsSomewhere` (`src/mind/traverse.ts`)
|
|
22
29
|
|
|
@@ -36,11 +43,18 @@ and are filtered during recognition.
|
|
|
36
43
|
The cover reports `moves` (its derivation's discrete work) and `accounted`; the
|
|
37
44
|
ladder prices both.
|
|
38
45
|
|
|
39
|
-
##
|
|
46
|
+
## Licensed premises (`src/mind/mechanisms/cover.ts`, `Licence` in `graph-search.ts`)
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
The synchronous search cannot run the async reads two of its rules need — a
|
|
49
|
+
concept target (a halo lookup) and a learnt connector between two answers (a
|
|
50
|
+
`bridge`). `offerConcepts` and `offerConnectors` OFFER the keys up front (cheap:
|
|
51
|
+
`hasNext`, the touching-site pairs and the N-ary allowances); the search ASKS
|
|
52
|
+
for an offered key only where it reaches it — a connector when its splice's two
|
|
53
|
+
premises meet, a concept target when the hop's asking form (held at the hop's
|
|
54
|
+
own cost) is popped. A cover that asked is provisional: `cover.run` grants the
|
|
55
|
+
asked keys and covers again, until a cover asks nothing — which is then the
|
|
56
|
+
cover every key resolved in advance would have made. The joins licensed by
|
|
57
|
+
ask-free rounds are kept across the re-covers.
|
|
44
58
|
|
|
45
59
|
## Provenance
|
|
46
60
|
|
|
@@ -54,3 +68,6 @@ independent evidence streams meet at one anchor — see
|
|
|
54
68
|
|
|
55
69
|
- `test/09-edges.test.mjs` — edge following and hop semantics
|
|
56
70
|
- `test/19-nd.test.mjs` — form rules and multi-hop chains
|
|
71
|
+
- `test/151` — connectors and concept hops resolved where the search reaches
|
|
72
|
+
them; a span's cheapest completion dominates (a hub's degree generates no
|
|
73
|
+
work)
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/alu/README.md
CHANGED
|
@@ -238,8 +238,16 @@ const sync = await prefetchResonance(resonance, spans);
|
|
|
238
238
|
```
|
|
239
239
|
|
|
240
240
|
The synchronous op callbacks never await — they read from the pre-resolved
|
|
241
|
-
snapshots.
|
|
242
|
-
|
|
241
|
+
snapshots. A named operation applied to the query's operand stream resolves
|
|
242
|
+
opposites ON DEMAND (`withOppositesOnDemand`). The kernel runs against a
|
|
243
|
+
snapshot of the opposites resolved so far. When it asked for one of its symbol
|
|
244
|
+
operands that is not resolved yet, that one is resolved through the host and the
|
|
245
|
+
pure kernel runs again. The result is the eager prefetch's, but only the
|
|
246
|
+
polymorphic inverse reads opposites. Before this, every symbol operand of ANY
|
|
247
|
+
operation paid one host call, and on Sema each is a halo-index query (30–200 ms
|
|
248
|
+
of a plain dialogue turn's parse that computed nothing). The public
|
|
249
|
+
`Mind.compute(name, operands)` path pre-resolves every symbol span before the
|
|
250
|
+
synchronous kernel runs, using the same discipline.
|
|
243
251
|
|
|
244
252
|
### The mind loop
|
|
245
253
|
|
package/src/alu/src/index.ts
CHANGED
package/src/alu/src/parser.ts
CHANGED
|
@@ -45,10 +45,9 @@ import type { Alu } from "./alu.js";
|
|
|
45
45
|
import {
|
|
46
46
|
type AluResonance,
|
|
47
47
|
type ConceptAnchor,
|
|
48
|
-
prefetchOpposites,
|
|
49
48
|
prefetchResonance,
|
|
49
|
+
withOppositesOnDemand,
|
|
50
50
|
} from "./resonance.js";
|
|
51
|
-
import { NO_RESONANCE } from "./operation.js";
|
|
52
51
|
import { int, real, symbol, symbolSpans, type Value } from "./value.js";
|
|
53
52
|
import { nonSpaceRuns } from "./text.js";
|
|
54
53
|
import { bytesEqual, latin1 } from "../../bytes.js";
|
|
@@ -573,10 +572,11 @@ export class QueryParser {
|
|
|
573
572
|
const symbols = picked.flatMap((t, k) =>
|
|
574
573
|
t.kind === "term" ? [args[k]] : []
|
|
575
574
|
);
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
575
|
+
const bytes = await withOppositesOnDemand(
|
|
576
|
+
this.resonance,
|
|
577
|
+
symbols,
|
|
578
|
+
(resonance) => this.alu.applyBytes(name, args, resonance),
|
|
579
|
+
);
|
|
580
580
|
if (bytes === null) return null;
|
|
581
581
|
if (
|
|
582
582
|
symbols.length === args.length && args.some((a) => bytesEqual(bytes, a))
|
package/src/alu/src/resonance.ts
CHANGED
|
@@ -104,6 +104,48 @@ export async function prefetchOpposites(
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/** {@link prefetchOpposites} resolved ON DEMAND: run `apply` against a
|
|
108
|
+
* synchronous snapshot that answers only the opposites already resolved, and
|
|
109
|
+
* when the computation ASKED for one of `symbols` that is not yet resolved,
|
|
110
|
+
* resolve it through the host and run `apply` again — until a run asks for
|
|
111
|
+
* nothing new. The result is `apply(prefetchOpposites(resonance, symbols))`
|
|
112
|
+
* exactly: an opposite outside `symbols` reads null in both, every one inside
|
|
113
|
+
* that the computation reads is the host's answer in both, and `apply` is
|
|
114
|
+
* pure, so a run that read the same answers returns the same bytes. What it
|
|
115
|
+
* saves is every host call no computation reads: only the polymorphic
|
|
116
|
+
* inverse reads opposites, yet the eager prefetch paid one per symbol operand
|
|
117
|
+
* for ANY operation — on SEMA a halo-index query each, measured at 30-200 ms
|
|
118
|
+
* of a plain dialogue turn's parse that computed nothing. */
|
|
119
|
+
export async function withOppositesOnDemand<R>(
|
|
120
|
+
resonance: AluResonance,
|
|
121
|
+
symbols: Iterable<Uint8Array>,
|
|
122
|
+
apply: (sync: ResonanceSync) => R,
|
|
123
|
+
): Promise<R> {
|
|
124
|
+
const allowed = new Set<string>();
|
|
125
|
+
for (const bytes of symbols) allowed.add(latin1(bytes));
|
|
126
|
+
const table = new Map<string, Uint8Array | null>();
|
|
127
|
+
const asked = new Map<string, Uint8Array>();
|
|
128
|
+
const sync: ResonanceSync = {
|
|
129
|
+
opposite: (bytes: Uint8Array) => {
|
|
130
|
+
const key = latin1(bytes);
|
|
131
|
+
if (!allowed.has(key)) return null;
|
|
132
|
+
const known = table.get(key);
|
|
133
|
+
if (known !== undefined) return known;
|
|
134
|
+
asked.set(key, bytes);
|
|
135
|
+
return null;
|
|
136
|
+
},
|
|
137
|
+
recogniseOp: () => null,
|
|
138
|
+
};
|
|
139
|
+
for (;;) {
|
|
140
|
+
const out = apply(sync);
|
|
141
|
+
if (asked.size === 0) return out;
|
|
142
|
+
for (const [key, bytes] of asked) {
|
|
143
|
+
table.set(key, (await resonance.opposite(bytes)) ?? null);
|
|
144
|
+
}
|
|
145
|
+
asked.clear();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
107
149
|
/** Pre-resolve BOTH capabilities a computation may need synchronously — the
|
|
108
150
|
* resonant opposite of a symbol (for the polymorphic inverse) AND the operation
|
|
109
151
|
* a symbol's MEANING names (for a higher-order nd op's function argument) — over
|
package/src/alu/test/alu.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import assert from "node:assert/strict";
|
|
|
9
9
|
import {
|
|
10
10
|
addBits,
|
|
11
11
|
Alu,
|
|
12
|
+
type AluHost,
|
|
12
13
|
type AluResonance,
|
|
13
14
|
asReal,
|
|
14
15
|
compareBits,
|
|
@@ -579,6 +580,45 @@ test("conceptAnchors exposes the operation vocabulary for resonant recognition",
|
|
|
579
580
|
}
|
|
580
581
|
});
|
|
581
582
|
|
|
583
|
+
test("a symbol's opposite is asked of the host only when the operation reads it", async () => {
|
|
584
|
+
// Only the polymorphic inverse reads an opposite. Resolving every symbol
|
|
585
|
+
// operand's opposite up front paid one host call per operand for ANY
|
|
586
|
+
// operation — on SEMA a halo-index query each, 30-200 ms of a plain
|
|
587
|
+
// dialogue turn's parse that computed nothing.
|
|
588
|
+
const asked: string[] = [];
|
|
589
|
+
const host: AluHost = {
|
|
590
|
+
meaningOf: async () => null,
|
|
591
|
+
continuation: async (b) => {
|
|
592
|
+
asked.push(dec(b));
|
|
593
|
+
return dec(b) === "large" ? enc("small") : null;
|
|
594
|
+
},
|
|
595
|
+
segment: (bytes) => {
|
|
596
|
+
const runs: Array<{ i: number; j: number }> = [];
|
|
597
|
+
for (let i = 0; i < bytes.length;) {
|
|
598
|
+
if (bytes[i] === 32) {
|
|
599
|
+
i++;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
let j = i;
|
|
603
|
+
while (j < bytes.length && bytes[j] !== 32) j++;
|
|
604
|
+
runs.push({ i, j });
|
|
605
|
+
i = j;
|
|
606
|
+
}
|
|
607
|
+
return runs;
|
|
608
|
+
},
|
|
609
|
+
reach: Number.POSITIVE_INFINITY,
|
|
610
|
+
};
|
|
611
|
+
const u = new Alu({}, host);
|
|
612
|
+
// An operation that does not read opposites: nothing computed, nothing asked.
|
|
613
|
+
assert.deepEqual(await u.parse(enc("sqrt large")), []);
|
|
614
|
+
assert.deepEqual(asked, []);
|
|
615
|
+
// The inverse reads it: asked once, and grounded exactly as before.
|
|
616
|
+
const out = await u.parse(enc("opposite large"));
|
|
617
|
+
assert.equal(out.length, 1);
|
|
618
|
+
assert.equal(dec(out[0].bytes), "small");
|
|
619
|
+
assert.deepEqual(asked, ["large"]);
|
|
620
|
+
});
|
|
621
|
+
|
|
582
622
|
test("prefetchRecognisedOps bridges async recognition to a sync map", async () => {
|
|
583
623
|
// A stub host resonance: "the rate of change of" means a derivative.
|
|
584
624
|
const stub: AluResonance = {
|
package/src/bytes.ts
CHANGED
|
@@ -35,11 +35,21 @@ export function concat2(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Latin-1 view of a byte span —
|
|
39
|
-
*
|
|
38
|
+
/** Latin-1 view of a byte span — ONE code unit per byte, so it is injective
|
|
39
|
+
* and safe as an exact cache key (every byte 0–255 maps to one code unit).
|
|
40
|
+
* Batched `String.fromCharCode`, chunked to stay within the engine's argument
|
|
41
|
+
* limit on long spans; the one definition every content-keyed memo uses.
|
|
42
|
+
* `apply` takes the typed array as its argument list directly — a spread
|
|
43
|
+
* walks it through the iterator protocol first, ~2.7× slower per short key. */
|
|
40
44
|
export function latin1(b: Uint8Array): string {
|
|
45
|
+
const n = b.length;
|
|
41
46
|
let s = "";
|
|
42
|
-
for (let
|
|
47
|
+
for (let i = 0; i < n; i += 4096) {
|
|
48
|
+
s += String.fromCharCode.apply(
|
|
49
|
+
null,
|
|
50
|
+
b.subarray(i, Math.min(i + 4096, n)) as unknown as number[],
|
|
51
|
+
);
|
|
52
|
+
}
|
|
43
53
|
return s;
|
|
44
54
|
}
|
|
45
55
|
|
package/src/canon.ts
CHANGED
|
@@ -44,6 +44,12 @@ const enc = new TextEncoder();
|
|
|
44
44
|
* deliberately conservative: punctuation, digits and word order are content
|
|
45
45
|
* and pass through untouched. */
|
|
46
46
|
export function textCanon(bytes: Uint8Array): Uint8Array {
|
|
47
|
+
if (isAscii(bytes)) return asciiCanon(bytes);
|
|
48
|
+
return unicodeCanon(bytes);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The general reading — the DEFINITION of {@link textCanon}. */
|
|
52
|
+
function unicodeCanon(bytes: Uint8Array): Uint8Array {
|
|
47
53
|
const s = dec
|
|
48
54
|
.decode(bytes)
|
|
49
55
|
.normalize("NFKC")
|
|
@@ -52,6 +58,40 @@ export function textCanon(bytes: Uint8Array): Uint8Array {
|
|
|
52
58
|
return enc.encode(s);
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
function isAscii(bytes: Uint8Array): boolean {
|
|
62
|
+
for (let i = 0; i < bytes.length; i++) if (bytes[i] >= 0x80) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** {@link unicodeCanon} on ASCII input, byte for byte, without the string
|
|
67
|
+
* round trip. Exact, not approximate: NFKC is the identity on ASCII, the
|
|
68
|
+
* lowercase of ASCII is A–Z → a–z, and the ASCII members of the regex's `\s`
|
|
69
|
+
* are TAB, LF, VT, FF, CR and SPACE — so an interior run of them becomes one
|
|
70
|
+
* space and an edge run stays verbatim, exactly as the regex rewrites it.
|
|
71
|
+
* The canonicalizer runs once per probed span on the recognition and join
|
|
72
|
+
* paths, so its constant is paid thousands of times per response; test/148
|
|
73
|
+
* pins the agreement over random ASCII and over the edge cases. */
|
|
74
|
+
function asciiCanon(bytes: Uint8Array): Uint8Array {
|
|
75
|
+
const n = bytes.length;
|
|
76
|
+
const out = new Uint8Array(n);
|
|
77
|
+
let o = 0;
|
|
78
|
+
const ws = (b: number) => b === 0x20 || (b >= 0x09 && b <= 0x0d);
|
|
79
|
+
for (let i = 0; i < n;) {
|
|
80
|
+
const b = bytes[i];
|
|
81
|
+
if (!ws(b)) {
|
|
82
|
+
out[o++] = b >= 0x41 && b <= 0x5a ? b + 0x20 : b;
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
let j = i;
|
|
87
|
+
while (j < n && ws(bytes[j])) j++;
|
|
88
|
+
if (i > 0 && j < n) out[o++] = 0x20;
|
|
89
|
+
else for (let k = i; k < j; k++) out[o++] = bytes[k];
|
|
90
|
+
i = j;
|
|
91
|
+
}
|
|
92
|
+
return o === n ? out : out.slice(0, o);
|
|
93
|
+
}
|
|
94
|
+
|
|
55
95
|
/** 32-bit FNV-1a over a canonical key — the integer the store's canon index
|
|
56
96
|
* is keyed on. Same construction as the node table's content hash; a
|
|
57
97
|
* collision is resolved by verifying canon(stored) === key, never trusted. */
|