pi-mega-compact 0.20.11 → 0.20.13

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 (84) hide show
  1. package/dist/config/vector-cortex-breakers.js +31 -0
  2. package/dist/config/vector-cortex.js +17 -26
  3. package/dist/config.js +1 -1
  4. package/dist/extensions/dashboard-server/api-contracts/vector-cortex-cache.js +11 -0
  5. package/dist/extensions/dashboard-server/route-dispatch.js +6 -0
  6. package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +1 -0
  7. package/dist/extensions/dashboard-server/routes-vector-cortex-crystals.js +65 -0
  8. package/dist/src/config/vector-cortex-breakers.js +31 -0
  9. package/dist/src/config/vector-cortex.js +17 -26
  10. package/dist/src/config.js +1 -1
  11. package/dist/src/vector-cortex/cache/_crystal-fixture.js +71 -0
  12. package/dist/src/vector-cortex/cache/crystal-emit.js +76 -0
  13. package/dist/src/vector-cortex/cache/crystal.js +201 -0
  14. package/dist/src/vector-cortex/cache/store.js +196 -0
  15. package/dist/vector-cortex/cache/_crystal-fixture.js +71 -0
  16. package/dist/vector-cortex/cache/crystal-emit.js +76 -0
  17. package/dist/vector-cortex/cache/crystal.js +201 -0
  18. package/dist/vector-cortex/cache/store.js +196 -0
  19. package/dist/vector-cortex/cache/types.js +73 -0
  20. package/extensions/dashboard-client/dist/assets/{AreaChart-BDMjyRQp.js → AreaChart-CB-U7ViX.js} +2 -2
  21. package/extensions/dashboard-client/dist/assets/{AreaChart-BDMjyRQp.js.map → AreaChart-CB-U7ViX.js.map} +1 -1
  22. package/extensions/dashboard-client/dist/assets/{BarChart-CqzPzOkj.js → BarChart-DnLw0fxA.js} +2 -2
  23. package/extensions/dashboard-client/dist/assets/{BarChart-CqzPzOkj.js.map → BarChart-DnLw0fxA.js.map} +1 -1
  24. package/extensions/dashboard-client/dist/assets/{CacheTab-Bo8SH9q8.js → CacheTab-CjyTVDiP.js} +2 -2
  25. package/extensions/dashboard-client/dist/assets/{CacheTab-Bo8SH9q8.js.map → CacheTab-CjyTVDiP.js.map} +1 -1
  26. package/extensions/dashboard-client/dist/assets/{EventsTab-DIEs6zC-.js → EventsTab-Bz3QEWam.js} +2 -2
  27. package/extensions/dashboard-client/dist/assets/{EventsTab-DIEs6zC-.js.map → EventsTab-Bz3QEWam.js.map} +1 -1
  28. package/extensions/dashboard-client/dist/assets/{HealthTab-3a8IjwIK.js → HealthTab-CUylEvEY.js} +2 -2
  29. package/extensions/dashboard-client/dist/assets/{HealthTab-3a8IjwIK.js.map → HealthTab-CUylEvEY.js.map} +1 -1
  30. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CdKwAcXo.js → MaintenanceTab-Cz_hj_E7.js} +2 -2
  31. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-CdKwAcXo.js.map → MaintenanceTab-Cz_hj_E7.js.map} +1 -1
  32. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-D2hHuNj-.js → MemoryMapTab-Dh8brIR1.js} +2 -2
  33. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-D2hHuNj-.js.map → MemoryMapTab-Dh8brIR1.js.map} +1 -1
  34. package/extensions/dashboard-client/dist/assets/{MetricsTab-CFhIhqrr.js → MetricsTab-CkqSXc6e.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{MetricsTab-CFhIhqrr.js.map → MetricsTab-CkqSXc6e.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/{OverviewTab-Dzp5ZoiA.js → OverviewTab-DGbJcx6d.js} +2 -2
  37. package/extensions/dashboard-client/dist/assets/{OverviewTab-Dzp5ZoiA.js.map → OverviewTab-DGbJcx6d.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{ReposTab-CFJT9mDR.js → ReposTab-BawS3nfF.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{ReposTab-CFJT9mDR.js.map → ReposTab-BawS3nfF.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{SessionsTab-BhP1z6_y.js → SessionsTab-CUVnmYks.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{SessionsTab-BhP1z6_y.js.map → SessionsTab-CUVnmYks.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{SetupTab-2Glh7MKk.js → SetupTab-cxTuyZNk.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{SetupTab-2Glh7MKk.js.map → SetupTab-cxTuyZNk.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DmwBDhkw.js → TimeSavedCard-Cv5SENkE.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DmwBDhkw.js.map → TimeSavedCard-Cv5SENkE.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{TurnsTab-CYIaOUUZ.js → TurnsTab-yl6BBG_F.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{TurnsTab-CYIaOUUZ.js.map → TurnsTab-yl6BBG_F.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/VectorCortexTab-94fqVjat.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/VectorCortexTab-94fqVjat.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{WikiTab-CP-JEd17.js → WikiTab-DKv-0xCQ.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{WikiTab-CP-JEd17.js.map → WikiTab-DKv-0xCQ.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{button-B1RhLsGs.js → button-CVPrO4UU.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{button-B1RhLsGs.js.map → button-CVPrO4UU.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{card-BLPT2-8G.js → card-BFLnQJEo.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{card-BLPT2-8G.js.map → card-BFLnQJEo.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DVOMJzL2.js → generateCategoricalChart-B-IUoLd1.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DVOMJzL2.js.map → generateCategoricalChart-B-IUoLd1.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{index-CuLdiHRl.js → index-Do749WlW.js} +3 -3
  59. package/extensions/dashboard-client/dist/assets/{index-CuLdiHRl.js.map → index-Do749WlW.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{switch-C5tqzhcl.js → switch-3uFnZmtq.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{switch-C5tqzhcl.js.map → switch-3uFnZmtq.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{toggle-HcM6W2Yl.js → toggle-C1rYLeXe.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{toggle-HcM6W2Yl.js.map → toggle-C1rYLeXe.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{useSSE-BtWUs2kL.js → useSSE-D1qzLzgR.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{useSSE-BtWUs2kL.js.map → useSSE-D1qzLzgR.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/index.html +1 -1
  67. package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
  68. package/extensions/dashboard-client/src/tabs/VectorCortexCrystalsCard.tsx +46 -0
  69. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +8 -0
  70. package/extensions/dashboard-client/src/types/vector-cortex.ts +20 -0
  71. package/extensions/dashboard-server/api-contracts/vector-cortex-cache.ts +53 -0
  72. package/extensions/dashboard-server/route-dispatch.ts +5 -0
  73. package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +6 -0
  74. package/extensions/dashboard-server/routes-vector-cortex-crystals.ts +74 -0
  75. package/package.json +1 -1
  76. package/src/config/vector-cortex-breakers.ts +32 -0
  77. package/src/config/vector-cortex.ts +31 -26
  78. package/src/config.ts +1 -0
  79. package/src/vector-cortex/cache/_crystal-fixture.ts +126 -0
  80. package/src/vector-cortex/cache/crystal-emit.ts +99 -0
  81. package/src/vector-cortex/cache/crystal.ts +217 -0
  82. package/src/vector-cortex/cache/store.ts +213 -0
  83. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD7vGaRS.js +0 -2
  84. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD7vGaRS.js.map +0 -1
@@ -0,0 +1,196 @@
1
+ /**
2
+ * vector-cortex/cache/store.ts — content-addressed, write-once crystal store
3
+ * (VC7A).
4
+ *
5
+ * WRITE ONCE, NEVER OVERWRITE. A key that already holds bytes is final. Writing
6
+ * the SAME bytes again is accepted and idempotent (two concurrent renders raced;
7
+ * both are right). Writing DIFFERENT bytes under the same key returns
8
+ * `CRY_KEY_COLLISION` and the stored crystal is left exactly as it was.
9
+ *
10
+ * Why refuse rather than take the newer bytes? Because the key already names
11
+ * everything the render depended on — covered ranges, their digest, the validated
12
+ * dependency high-water, the renderer, the profile. If two renders of that same
13
+ * identity disagree, the renderer is NOT deterministic, and that is a bug to
14
+ * surface loudly, not to paper over by picking a winner. Last-write-wins would
15
+ * make the failure invisible and let a corrupted render silently displace a good
16
+ * one. So the store is a one-way ratchet per key.
17
+ *
18
+ * CONTENT ADDRESSED. Every crystal carries the SHA-256 of exactly its own bytes
19
+ * (bare lowercase hex, matching `ExactShardV1.digest`), recomputed HERE from the
20
+ * bytes rather than trusted from the caller — a caller-supplied digest would let
21
+ * a mismatched pair be stored and later "verify" against itself.
22
+ *
23
+ * ATOMIC COMMIT (the crash invariant). Writes go through a staging slot and are
24
+ * only published by `commit`. A write interrupted before commit leaves the
25
+ * staging entry orphaned and the visible map untouched, so `read` can never
26
+ * observe a partial crystal. On restart, `recover()` DISCARDS staged entries —
27
+ * it never promotes them — and a fresh write then produces exactly one valid
28
+ * crystal. This models the real filesystem shape (write temp file → fsync →
29
+ * rename) without doing any I/O here: persistence is the runtime's job, and
30
+ * `src/` stays storage-free and pure (PREVENT-PI-004).
31
+ *
32
+ * MODE C. `setAvailable(false)` marks the store unavailable: reads serve nothing
33
+ * and writes refuse with `CRY_STORE_UNAVAILABLE`. Mode C is a real triad state,
34
+ * not an error path — the caller must fall back to a fresh render and disclose
35
+ * that nothing came from cache.
36
+ *
37
+ * No console, no network (PREVENT-PI-004 / PREVENT-011).
38
+ */
39
+ import { createHash } from "node:crypto";
40
+ /** SHA-256 over bytes, BARE lowercase hex (the content address). */
41
+ export function contentAddress(bytes) {
42
+ return createHash("sha256").update(bytes).digest("hex");
43
+ }
44
+ /** Byte equality without allocating — a length check first, then a scan. */
45
+ function bytesEqual(a, b) {
46
+ if (a.length !== b.length)
47
+ return false;
48
+ for (let i = 0; i < a.length; i += 1)
49
+ if (a[i] !== b[i])
50
+ return false;
51
+ return true;
52
+ }
53
+ /**
54
+ * An in-memory, content-addressed, write-once crystal store.
55
+ *
56
+ * Deliberately holds no filesystem handle: the process-local map IS the store as
57
+ * far as `src/` is concerned, and durability is layered on by the runtime. That
58
+ * keeps the write-once/collision arithmetic testable end-to-end with real
59
+ * modules and no I/O mocking.
60
+ */
61
+ export class CrystalStore {
62
+ /** Published crystals, keyed by canonical key digest. */
63
+ committed = new Map();
64
+ /** Staged-but-uncommitted writes — invisible to `read` until committed. */
65
+ staged = new Map();
66
+ available = true;
67
+ hits = 0;
68
+ misses = 0;
69
+ hitBytes = 0;
70
+ writes = 0;
71
+ duplicateWrites = 0;
72
+ collisions = 0;
73
+ /** Freeze a crystal object for a key/bytes pair (digest computed here). */
74
+ static freeze(keyDigest, bytes, key) {
75
+ const copy = new Uint8Array(bytes);
76
+ return {
77
+ schema: "crystal-v1",
78
+ keyDigest,
79
+ bytes: copy,
80
+ contentDigest: contentAddress(copy),
81
+ byteCount: copy.length,
82
+ key,
83
+ };
84
+ }
85
+ /** Toggle store availability (mode C when false). */
86
+ setAvailable(value) {
87
+ this.available = value;
88
+ }
89
+ isAvailable() {
90
+ return this.available;
91
+ }
92
+ /**
93
+ * Stage a write without publishing it. Returns the staging handle (the key
94
+ * digest) so a test — or a crash — can leave it uncommitted.
95
+ */
96
+ stage(crystal) {
97
+ if (!this.available) {
98
+ return { ok: false, code: "CRY_STORE_UNAVAILABLE", contentDigest: crystal.contentDigest };
99
+ }
100
+ this.staged.set(crystal.keyDigest, crystal);
101
+ return { ok: true, written: false, contentDigest: crystal.contentDigest };
102
+ }
103
+ /**
104
+ * Publish a staged write. Enforces write-once at the commit point: an existing
105
+ * key with identical bytes is idempotent, with different bytes is a collision
106
+ * and is NEVER overwritten.
107
+ */
108
+ commit(keyDigest) {
109
+ const pending = this.staged.get(keyDigest);
110
+ if (pending === undefined) {
111
+ return { ok: false, code: "CRY_STORE_UNAVAILABLE", contentDigest: "" };
112
+ }
113
+ this.staged.delete(keyDigest);
114
+ if (!this.available) {
115
+ return { ok: false, code: "CRY_STORE_UNAVAILABLE", contentDigest: pending.contentDigest };
116
+ }
117
+ const existing = this.committed.get(keyDigest);
118
+ if (existing !== undefined) {
119
+ if (bytesEqual(existing.bytes, pending.bytes)) {
120
+ this.duplicateWrites += 1;
121
+ return { ok: true, written: false, contentDigest: existing.contentDigest };
122
+ }
123
+ this.collisions += 1;
124
+ return { ok: false, code: "CRY_KEY_COLLISION", contentDigest: existing.contentDigest };
125
+ }
126
+ this.committed.set(keyDigest, pending);
127
+ this.writes += 1;
128
+ return { ok: true, written: true, contentDigest: pending.contentDigest };
129
+ }
130
+ /** Stage + commit in one step (the normal path). */
131
+ write(crystal) {
132
+ const staged = this.stage(crystal);
133
+ if (!staged.ok)
134
+ return staged;
135
+ return this.commit(crystal.keyDigest);
136
+ }
137
+ /**
138
+ * Restart recovery: DISCARD every staged entry. A write interrupted before
139
+ * commit is never promoted — a fresh write afterwards produces exactly one
140
+ * valid crystal. Returns how many partial writes were dropped.
141
+ */
142
+ recover() {
143
+ const dropped = this.staged.size;
144
+ this.staged.clear();
145
+ return dropped;
146
+ }
147
+ /** Read a committed crystal. Mode C (unavailable) serves nothing. */
148
+ read(keyDigest) {
149
+ if (!this.available) {
150
+ this.misses += 1;
151
+ return undefined;
152
+ }
153
+ const found = this.committed.get(keyDigest);
154
+ if (found === undefined) {
155
+ this.misses += 1;
156
+ return undefined;
157
+ }
158
+ this.hits += 1;
159
+ this.hitBytes += found.byteCount;
160
+ return found;
161
+ }
162
+ /** Whether a key holds a committed crystal (does not count as a read). */
163
+ has(keyDigest) {
164
+ return this.available && this.committed.has(keyDigest);
165
+ }
166
+ /** Number of writes staged but not yet committed (0 after `recover`). */
167
+ pendingCount() {
168
+ return this.staged.size;
169
+ }
170
+ /**
171
+ * The triad mode the store is currently in: C when unavailable, A once a read
172
+ * has been served from the store, B while every read has forced a fresh render.
173
+ */
174
+ mode() {
175
+ if (!this.available)
176
+ return "C";
177
+ return this.hits > 0 ? "A" : "B";
178
+ }
179
+ /** Reader-only aggregate for the dashboard seam — counts and bytes only. */
180
+ stats() {
181
+ let totalBytes = 0;
182
+ for (const c of this.committed.values())
183
+ totalBytes += c.byteCount;
184
+ return {
185
+ mode: this.mode(),
186
+ crystalCount: this.committed.size,
187
+ totalBytes,
188
+ hits: this.hits,
189
+ misses: this.misses,
190
+ hitBytes: this.hitBytes,
191
+ writes: this.writes,
192
+ duplicateWrites: this.duplicateWrites,
193
+ collisions: this.collisions,
194
+ };
195
+ }
196
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * cache/_crystal-fixture.ts — conformance fixture I/O for VC7A crystal rows.
3
+ *
4
+ * Sibling of `../heal/_restore-fixture.ts`, same job for a different corpus:
5
+ * turn canonical JSON back into the REAL production types the cache modules
6
+ * consume. Fixtures cannot express bigints, so `dependencyHighWater` and the
7
+ * span seq bounds are stored as numbers and converted here — if that conversion
8
+ * were lossy the encoded keys would diverge and the acceptance rows would fail
9
+ * loudly, which is exactly the guarantee an identity sprint needs.
10
+ *
11
+ * No mocks, no stubs, no parallel "test shape": the decoded objects ARE
12
+ * `CrystalKeyV1` / `DagSpan` and are fed verbatim into `encodeCrystalKey` and
13
+ * `CrystalStore`.
14
+ */
15
+ import { readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import assert from "node:assert/strict";
18
+ import { computeCoveredDigest } from "./crystal.js";
19
+ import { V2, readManifest } from "../heal/_acceptance-fixture.js";
20
+ /** Read one registered cache-crystal fixture (asserting it IS registered). */
21
+ export function crystalFixture(id) {
22
+ const m = readManifest();
23
+ const row = m.fixtures.find((f) => f.id === id && f.path.startsWith("cache-crystals/"));
24
+ assert.ok(row, `fixture ${id} registered under cache-crystals/ in manifest`);
25
+ return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
26
+ }
27
+ /** JSON number seq bounds -> the bigint bounds `DagSpan` declares. */
28
+ export function decodeSpan(s) {
29
+ return {
30
+ sessionId: s.sessionId,
31
+ startSeq: BigInt(s.startSeq),
32
+ endSeq: BigInt(s.endSeq),
33
+ startByte: s.startByte,
34
+ endByte: s.endByte,
35
+ digest: s.digest,
36
+ };
37
+ }
38
+ /**
39
+ * Reconstitute a real `CrystalKeyV1`. `coveredDigest` is DERIVED from the ranges
40
+ * rather than carried in the corpus: the covered digest is a function of the
41
+ * ranges, so storing it would let a fixture assert a self-inconsistent identity
42
+ * that the encoder would silently re-derive anyway.
43
+ */
44
+ export function decodeKey(k) {
45
+ const sourceRanges = k.sourceRanges.map(decodeSpan);
46
+ return {
47
+ profileId: k.profileId,
48
+ profileVersion: k.profileVersion,
49
+ requestDigest: k.requestDigest,
50
+ rendererVersion: k.rendererVersion,
51
+ dependencyHighWater: BigInt(k.dependencyHighWater),
52
+ sourceRanges,
53
+ coveredDigest: computeCoveredDigest(sourceRanges),
54
+ };
55
+ }
56
+ /** Flag-pinned wrapper: VC7A gated by MEGACOMPACT_VC7A (defaults ON). */
57
+ export function withVc7aFlag(value, fn) {
58
+ return () => {
59
+ const saved = process.env.MEGACOMPACT_VC7A;
60
+ process.env.MEGACOMPACT_VC7A = value;
61
+ try {
62
+ fn();
63
+ }
64
+ finally {
65
+ if (saved === undefined)
66
+ delete process.env.MEGACOMPACT_VC7A;
67
+ else
68
+ process.env.MEGACOMPACT_VC7A = saved;
69
+ }
70
+ };
71
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * vector-cortex/cache/crystal-emit.ts — VC7A event reporter seam.
3
+ *
4
+ * Mirrors `../heal/restore-emit.ts`: a thin `safe()` wrapper around an optional
5
+ * injected `emit` (unit tests pass `undefined` and stay pure), and the two event
6
+ * names the sprint spec requires verbatim:
7
+ * - `vector_cortex_crystal_written` — a crystal was frozen and published.
8
+ * - `vector_cortex_crystal_collision` — a same-key, different-bytes write was
9
+ * refused.
10
+ *
11
+ * FLAG SEMANTICS. `encodeCrystalKey` and every `CrystalStore` operation are PURE
12
+ * and run REGARDLESS of `MEGACOMPACT_VC7A`. The flag gates ONLY this reporting +
13
+ * dashboard seam: with the flag off we still key crystals identically, still
14
+ * write once, and still refuse collisions — we just do not announce it under the
15
+ * VC7A event namespace, and the dashboard reports `enabled:false` + mode C. That
16
+ * is what makes flag-off byte-identical to VC6C: the arithmetic is never skipped,
17
+ * only the emission.
18
+ *
19
+ * PAYLOAD DISCIPLINE. These events carry the KEY DIGEST, byte COUNTS, and the
20
+ * failure code — never the frozen bytes, never covered source text, never the
21
+ * span digests of user content. A crystal is a rendered prompt: an unguarded
22
+ * `payload` field here would dump the entire framed conversation into a log file
23
+ * (SECURITY_PRIVACY — the exact ledger is not diagnostic data).
24
+ *
25
+ * No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
26
+ * is a structured JSON event with `ts` + `event`.
27
+ */
28
+ import { VC7A_ENABLED } from "../../config/vector-cortex.js";
29
+ /** Run `fn` only when an emit exists; a reporting failure is never fatal. */
30
+ function safe(emit, fn) {
31
+ if (emit === undefined)
32
+ return;
33
+ try {
34
+ fn(emit);
35
+ }
36
+ catch {
37
+ // Non-fatal: a reporting failure must never break the agent loop.
38
+ }
39
+ }
40
+ /** The event names VC7A emits, exported for the dashboard seam and tests. */
41
+ export const CRYSTAL_EVENT_NAMES = [
42
+ "vector_cortex_crystal_written",
43
+ "vector_cortex_crystal_collision",
44
+ ];
45
+ /**
46
+ * Report a published crystal. Key digest + byte count + mode only — enough to
47
+ * see whether the cache is filling and being hit, without disclosing what was
48
+ * frozen.
49
+ */
50
+ export function reportCrystalWritten(emit, payload) {
51
+ if (!VC7A_ENABLED())
52
+ return;
53
+ safe(emit, (e) => e("vector_cortex_crystal_written", {
54
+ ts: undefined,
55
+ event: "vector_cortex_crystal_written",
56
+ keyDigest: payload.keyDigest,
57
+ byteCount: payload.byteCount,
58
+ mode: payload.mode,
59
+ }));
60
+ }
61
+ /**
62
+ * Report a refused write. A collision means two renders of the SAME identity
63
+ * disagreed — a determinism bug — so the code is surfaced rather than swallowed.
64
+ * Suppressed under flag-off.
65
+ */
66
+ export function reportCrystalCollision(emit, payload) {
67
+ if (!VC7A_ENABLED())
68
+ return;
69
+ safe(emit, (e) => e("vector_cortex_crystal_collision", {
70
+ ts: undefined,
71
+ event: "vector_cortex_crystal_collision",
72
+ keyDigest: payload.keyDigest,
73
+ code: payload.code,
74
+ mode: payload.mode,
75
+ }));
76
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * vector-cortex/cache/crystal.ts — canonical crystal key encoding (VC7A).
3
+ *
4
+ * Turns a `CrystalKeyV1` into a single stable digest. Everything about the
5
+ * encoding exists to make two properties simultaneously true:
6
+ *
7
+ * 1. IDENTICAL INPUTS ⇒ IDENTICAL KEY, regardless of how the caller ordered
8
+ * its ranges or which host produced them. Hence the explicit sort and the
9
+ * length-prefixed field framing below.
10
+ * 2. ANY IDENTITY CHANGE ⇒ DIFFERENT KEY, with no accidental aliasing. Hence
11
+ * length prefixes rather than delimiters: a delimiter-joined encoding lets
12
+ * an attacker (or an unlucky session name) push a separator into a field
13
+ * and forge a collision — `("a|b", "c")` and `("a", "b|c")` hash the same.
14
+ * Prefixing every variable-length field with its byte length makes the
15
+ * encoding injective, so a collision requires an actual SHA-256 collision.
16
+ *
17
+ * WHAT IS DELIBERATELY ABSENT. The global ledger frontier. It is not a
18
+ * parameter, it is not readable from here, and there is no code path that could
19
+ * fold it in. An unrelated append advances the frontier constantly; including it
20
+ * would invalidate every crystal on every turn and the cache would never hit.
21
+ * The key covers what the render DEPENDED ON, not what the world has since done.
22
+ *
23
+ * OVERLAP IS REJECTED, NOT MERGED. Two overlapping ranges in the same session
24
+ * make "the covered bytes" ambiguous: the overlap region would be hashed twice,
25
+ * and if the two spans pinned different digests the key would silently encode a
26
+ * contradiction. `CRY_RANGE_OVERLAP` fails the key closed instead. Ranges in
27
+ * DIFFERENT sessions never conflict — they cover disjoint byte streams by
28
+ * construction — so cross-session keys are legal and common.
29
+ *
30
+ * PURE. No clock, no storage, no console, no network (PREVENT-PI-004 /
31
+ * PREVENT-011). Runs identically with `MEGACOMPACT_VC7A` on or off — the flag
32
+ * gates only the reporter/dashboard seam in `crystal-emit.ts`.
33
+ */
34
+ import { createHash } from "node:crypto";
35
+ import { CRYSTAL_LIMIT_BYTES, CRYSTAL_LIMIT_RANGES, } from "./types.js";
36
+ /** Encoding version, folded into the digest so a future change cannot alias. */
37
+ const KEY_ENCODING_VERSION = "crystal-key-v1";
38
+ /**
39
+ * Length-prefixed field append. `<byteLength>:<bytes>` makes the concatenation
40
+ * injective, so no combination of field contents can impersonate another.
41
+ */
42
+ function field(parts, value) {
43
+ parts.push(`${Buffer.byteLength(value, "utf8")}:${value}`);
44
+ }
45
+ /**
46
+ * Total order over covered ranges: session, then start seq, then start byte.
47
+ *
48
+ * Ordering by SOURCE START (not by insertion, not by digest) is what makes the
49
+ * key independent of how the planner happened to enumerate its spans. `sessionId`
50
+ * leads because seq numbers are only comparable within a session.
51
+ */
52
+ export function compareSpans(a, b) {
53
+ if (a.sessionId !== b.sessionId)
54
+ return a.sessionId < b.sessionId ? -1 : 1;
55
+ if (a.startSeq !== b.startSeq)
56
+ return a.startSeq < b.startSeq ? -1 : 1;
57
+ if (a.startByte !== b.startByte)
58
+ return a.startByte - b.startByte;
59
+ if (a.endSeq !== b.endSeq)
60
+ return a.endSeq < b.endSeq ? -1 : 1;
61
+ return a.endByte - b.endByte;
62
+ }
63
+ /** Sort covered ranges into canonical order (never mutates the input array). */
64
+ export function sortSpans(spans) {
65
+ return [...spans].sort(compareSpans);
66
+ }
67
+ /** A range is malformed if either bound runs backwards or a byte bound is negative. */
68
+ function isMalformed(s) {
69
+ return (s.endSeq < s.startSeq ||
70
+ s.startByte < 0 ||
71
+ s.endByte < s.startByte ||
72
+ !Number.isSafeInteger(s.startByte) ||
73
+ !Number.isSafeInteger(s.endByte));
74
+ }
75
+ /**
76
+ * Byte-range overlap between two spans of the SAME session. Byte bounds are
77
+ * half-open (`[startByte, endByte)`), so touching ranges (`a.end === b.start`)
78
+ * are adjacent, not overlapping, and are legal.
79
+ */
80
+ function overlaps(a, b) {
81
+ return a.sessionId === b.sessionId && a.startByte < b.endByte && b.startByte < a.endByte;
82
+ }
83
+ /**
84
+ * Validate covered ranges: non-empty, bounded, well-formed, and disjoint within
85
+ * each session. Returns deduplicated codes in a deterministic order.
86
+ */
87
+ export function validateRanges(spans) {
88
+ const codes = new Set();
89
+ if (spans.length === 0)
90
+ codes.add("CRY_RANGE_EMPTY");
91
+ if (spans.length > CRYSTAL_LIMIT_RANGES)
92
+ codes.add("CRY_KEY_LIMIT");
93
+ let totalBytes = 0;
94
+ for (const s of spans) {
95
+ if (isMalformed(s))
96
+ codes.add("CRY_RANGE_INVALID");
97
+ else
98
+ totalBytes += s.endByte - s.startByte;
99
+ }
100
+ if (totalBytes > CRYSTAL_LIMIT_BYTES)
101
+ codes.add("CRY_KEY_LIMIT");
102
+ // Sorted order makes overlap a neighbour check within each session run.
103
+ const sorted = sortSpans(spans);
104
+ for (let i = 1; i < sorted.length; i += 1) {
105
+ const prev = sorted[i - 1];
106
+ const cur = sorted[i];
107
+ if (prev !== undefined && cur !== undefined && overlaps(prev, cur)) {
108
+ codes.add("CRY_RANGE_OVERLAP");
109
+ break;
110
+ }
111
+ }
112
+ const order = [
113
+ "CRY_RANGE_EMPTY",
114
+ "CRY_RANGE_INVALID",
115
+ "CRY_RANGE_OVERLAP",
116
+ "CRY_KEY_LIMIT",
117
+ ];
118
+ return order.filter((c) => codes.has(c));
119
+ }
120
+ /**
121
+ * The covered-bytes digest: SHA-256 over the SORTED ranges' identities and their
122
+ * pinned span digests, `sha256:` prefixed (matching the `DagSpan.digest`
123
+ * convention the ranges themselves carry).
124
+ *
125
+ * The span digests are what make this sensitive to a single covered BYTE: the
126
+ * ranges alone would be identical if a byte inside an unchanged range mutated,
127
+ * but the span's pinned digest would not be (CRY-COVERED-002).
128
+ */
129
+ export function computeCoveredDigest(spans) {
130
+ const h = createHash("sha256");
131
+ for (const s of sortSpans(spans)) {
132
+ const parts = [];
133
+ field(parts, s.sessionId);
134
+ field(parts, s.startSeq.toString());
135
+ field(parts, s.endSeq.toString());
136
+ field(parts, String(s.startByte));
137
+ field(parts, String(s.endByte));
138
+ field(parts, s.digest);
139
+ h.update(parts.join(""), "utf8");
140
+ }
141
+ return `sha256:${h.digest("hex")}`;
142
+ }
143
+ /**
144
+ * Canonical key bytes. Field order is fixed and every field is length-prefixed;
145
+ * the ranges are emitted in canonical sort order with an explicit count so a
146
+ * key with N ranges can never encode the same bytes as one with M.
147
+ */
148
+ export function encodeCrystalKeyBytes(key) {
149
+ const parts = [];
150
+ field(parts, KEY_ENCODING_VERSION);
151
+ field(parts, key.profileId);
152
+ field(parts, key.profileVersion);
153
+ field(parts, key.requestDigest);
154
+ field(parts, key.rendererVersion);
155
+ field(parts, key.dependencyHighWater.toString());
156
+ field(parts, key.coveredDigest);
157
+ const sorted = sortSpans(key.sourceRanges);
158
+ field(parts, String(sorted.length));
159
+ for (const s of sorted) {
160
+ field(parts, s.sessionId);
161
+ field(parts, s.startSeq.toString());
162
+ field(parts, s.endSeq.toString());
163
+ field(parts, String(s.startByte));
164
+ field(parts, String(s.endByte));
165
+ field(parts, s.digest);
166
+ }
167
+ return parts.join("");
168
+ }
169
+ /**
170
+ * Build the canonical key digest for a crystal identity.
171
+ *
172
+ * The returned `key` carries the ranges in canonical sorted order and the
173
+ * RE-DERIVED `coveredDigest`, so a caller that supplied a stale or wrong covered
174
+ * digest cannot mint a key that disagrees with its own ranges. The digest itself
175
+ * is bare lowercase hex (it addresses an identity, not source bytes).
176
+ */
177
+ export function encodeCrystalKey(key) {
178
+ const codes = validateRanges(key.sourceRanges);
179
+ if (codes.length > 0)
180
+ return { ok: false, codes };
181
+ const normalized = {
182
+ ...key,
183
+ sourceRanges: sortSpans(key.sourceRanges),
184
+ coveredDigest: computeCoveredDigest(key.sourceRanges),
185
+ };
186
+ const keyDigest = createHash("sha256")
187
+ .update(encodeCrystalKeyBytes(normalized), "utf8")
188
+ .digest("hex");
189
+ return { ok: true, keyDigest, key: normalized };
190
+ }
191
+ /**
192
+ * Whether two identities are the same crystal. Used by invalidation fixtures to
193
+ * state the sprint invariant directly: the key changes IFF an identity field
194
+ * changes — an unrelated frontier append is not an identity field, so it cannot
195
+ * appear here at all.
196
+ */
197
+ export function sameCrystalKey(a, b) {
198
+ const ea = encodeCrystalKey(a);
199
+ const eb = encodeCrystalKey(b);
200
+ return ea.ok && eb.ok && ea.keyDigest === eb.keyDigest;
201
+ }