@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.
Files changed (111) hide show
  1. package/AGENTS.md +7 -7
  2. package/dist/src/alu/src/index.d.ts +1 -1
  3. package/dist/src/alu/src/index.js +1 -1
  4. package/dist/src/alu/src/parser.js +2 -6
  5. package/dist/src/alu/src/resonance.d.ts +13 -0
  6. package/dist/src/alu/src/resonance.js +41 -0
  7. package/dist/src/alu/test/alu.test.js +39 -0
  8. package/dist/src/bytes.d.ts +6 -2
  9. package/dist/src/bytes.js +10 -4
  10. package/dist/src/canon.js +44 -0
  11. package/dist/src/geometry.d.ts +19 -1
  12. package/dist/src/geometry.js +125 -141
  13. package/dist/src/meter.d.ts +27 -0
  14. package/dist/src/meter.js +28 -1
  15. package/dist/src/mind/articulation.js +14 -1
  16. package/dist/src/mind/attention.d.ts +12 -0
  17. package/dist/src/mind/attention.js +44 -16
  18. package/dist/src/mind/bridge.js +3 -3
  19. package/dist/src/mind/derivation.d.ts +40 -0
  20. package/dist/src/mind/derivation.js +34 -0
  21. package/dist/src/mind/graph-search.d.ts +89 -15
  22. package/dist/src/mind/graph-search.js +345 -174
  23. package/dist/src/mind/learning.js +1 -1
  24. package/dist/src/mind/mechanisms/cover.d.ts +19 -3
  25. package/dist/src/mind/mechanisms/cover.js +101 -58
  26. package/dist/src/mind/mechanisms/recall.js +0 -1
  27. package/dist/src/mind/mind.js +2 -2
  28. package/dist/src/mind/pipeline.d.ts +5 -1
  29. package/dist/src/mind/pipeline.js +175 -87
  30. package/dist/src/mind/primitives.d.ts +25 -5
  31. package/dist/src/mind/primitives.js +107 -44
  32. package/dist/src/mind/reasoning.d.ts +18 -4
  33. package/dist/src/mind/reasoning.js +445 -321
  34. package/dist/src/mind/recognition.js +55 -73
  35. package/dist/src/mind/resonance.js +1 -11
  36. package/dist/src/mind/traverse.d.ts +3 -3
  37. package/dist/src/mind/traverse.js +3 -3
  38. package/dist/src/mind/types.d.ts +7 -1
  39. package/dist/src/store-sqlite.d.ts +25 -0
  40. package/dist/src/store-sqlite.js +89 -1
  41. package/dist/src/store.d.ts +48 -4
  42. package/dist/src/store.js +86 -6
  43. package/docs/INDEX.md +18 -18
  44. package/docs/INVARIANTS.md +16 -16
  45. package/docs/architecture/bounded-reads.md +1 -1
  46. package/docs/architecture/caches.md +5 -4
  47. package/docs/architecture/closure.md +45 -5
  48. package/docs/architecture/cost-model.md +16 -0
  49. package/docs/architecture/factored-machinery.md +14 -13
  50. package/docs/architecture/fold-contract.md +51 -1
  51. package/docs/architecture/mechanism-market.md +21 -0
  52. package/docs/architecture/memoization.md +3 -3
  53. package/docs/architecture/meter.md +2 -1
  54. package/docs/architecture/saturation.md +12 -0
  55. package/docs/architecture/store.md +25 -2
  56. package/docs/failures/tempting-but-wrong.md +13 -2
  57. package/docs/harness/gates.md +12 -10
  58. package/docs/mechanisms/cover.md +23 -6
  59. package/jsr.json +1 -1
  60. package/package.json +1 -1
  61. package/src/alu/README.md +10 -2
  62. package/src/alu/src/index.ts +1 -0
  63. package/src/alu/src/parser.ts +6 -6
  64. package/src/alu/src/resonance.ts +42 -0
  65. package/src/alu/test/alu.test.ts +40 -0
  66. package/src/bytes.ts +13 -3
  67. package/src/canon.ts +40 -0
  68. package/src/geometry.ts +183 -154
  69. package/src/meter.ts +28 -1
  70. package/src/mind/articulation.ts +14 -2
  71. package/src/mind/attention.ts +47 -25
  72. package/src/mind/bridge.ts +3 -3
  73. package/src/mind/derivation.ts +77 -0
  74. package/src/mind/graph-search.ts +449 -221
  75. package/src/mind/learning.ts +1 -7
  76. package/src/mind/match.ts +1 -2
  77. package/src/mind/mechanisms/cast.ts +1 -2
  78. package/src/mind/mechanisms/cover.ts +149 -84
  79. package/src/mind/mechanisms/extraction.ts +1 -2
  80. package/src/mind/mechanisms/prefix-completion.ts +1 -1
  81. package/src/mind/mechanisms/recall.ts +1 -3
  82. package/src/mind/mechanisms/reference.ts +1 -1
  83. package/src/mind/mind.ts +5 -30
  84. package/src/mind/pipeline.ts +206 -102
  85. package/src/mind/primitives.ts +119 -43
  86. package/src/mind/reasoning.ts +558 -413
  87. package/src/mind/recognition.ts +49 -65
  88. package/src/mind/resonance.ts +2 -16
  89. package/src/mind/trace.ts +1 -1
  90. package/src/mind/traverse.ts +3 -3
  91. package/src/mind/types.ts +9 -11
  92. package/src/store-sqlite.ts +92 -1
  93. package/src/store.ts +113 -7
  94. package/test/105-derive-through-reports-its-refusal.test.mjs +8 -5
  95. package/test/106-the-join-fires.test.mjs +21 -0
  96. package/test/111-the-cover-assembly-is-counted.test.mjs +8 -5
  97. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +18 -12
  98. package/test/136-the-two-named-limits.test.mjs +3 -2
  99. package/test/137-the-law-lives-once-and-below.test.mjs +21 -0
  100. package/test/148-exact-shortcuts-agree.test.mjs +188 -0
  101. package/test/149-the-closure-engine.test.mjs +138 -0
  102. package/test/150-the-join-is-output-sensitive.test.mjs +66 -0
  103. package/test/151-the-cover-pays-for-what-it-reaches.test.mjs +142 -0
  104. package/test/152-the-read-side-names-as-the-write-side.test.mjs +146 -0
  105. package/test/153-a-cheaper-bound-is-looked-at-first.test.mjs +155 -0
  106. package/test/24-generalization.test.mjs +32 -0
  107. package/test/36-bloom.test.mjs +53 -0
  108. package/test/37-cluster-dispersion-fusion.test.mjs +75 -0
  109. package/test/48-recognise-turn-connective.test.mjs +3 -2
  110. package/test/55-cost-meter.test.mjs +4 -4
  111. 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. Nothing
178
- in this repository is that today.
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.
@@ -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`–`147`). §14–25 (pipeline), §64 (derived thresholds), AGENTS.md §2
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
- ```bash
22
- node bench/profile-inference.mjs # add [n] to limit probes
23
- node bench/profile-inference.mjs --trace # trace is a debugging aid, not product
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
- Guards without trace: counters exact and diffable between COLD runs; phases nest
27
- (not disjoint — each phase is charged by its own layer); shared analyses charged
28
- to themselves, not to the first toucher; millisecond fields are
29
- non-deterministic hints only. With `--trace`, recognition idempotence still
30
- holds (`test/42`). `src/meter.ts`, `docs/architecture/meter.md`, §55,
31
- `AGENTS.md` §6.
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
 
@@ -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` / `resolveConcepts`)
19
- at `CONCEPT` (10), borrowing a synonym's continuation.
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
- ## Pre-resolution (`src/mind/mechanisms/cover.ts`)
46
+ ## Licensed premises (`src/mind/mechanisms/cover.ts`, `Licence` in `graph-search.ts`)
40
47
 
41
- `resolveConcepts` and `resolveConnectors` pre-resolve the async maps the
42
- synchronous search cannot gather: concept targets and learnt connectors, keyed
43
- by node pair. Bridges (`bridge`) splice connectors between rewrites.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://jsr.io/schema/config-file.v1.json",
3
3
  "name": "@hviana/sema",
4
- "version": "0.8.9",
4
+ "version": "0.9.1",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.8.9",
3
+ "version": "0.9.1",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "repository": {
6
6
  "type": "git",
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. The public `Mind.compute(name, operands)` path pre-resolves every
242
- symbol span before the synchronous kernel runs, using the same discipline.
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
 
@@ -51,6 +51,7 @@ export {
51
51
  prefetchOpposites,
52
52
  prefetchRecognisedOps,
53
53
  prefetchResonance,
54
+ withOppositesOnDemand,
54
55
  } from "./resonance.js";
55
56
 
56
57
  export {
@@ -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 resonance = symbols.length > 0
577
- ? await prefetchOpposites(this.resonance, symbols)
578
- : NO_RESONANCE;
579
- const bytes = this.alu.applyBytes(name, args, resonance);
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))
@@ -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
@@ -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 — a stable, lossless string key for chart
39
- * memoization (every byte 0–255 maps to one code unit). */
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 k = 0; k < b.length; k++) s += String.fromCharCode(b[k]);
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. */