pi-mega-compact 0.20.7 → 0.20.9
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/dist/config/vector-cortex.js +25 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex-heal.js +11 -0
- package/dist/extensions/dashboard-server/route-dispatch.js +104 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-heal.js +86 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +3 -0
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +3 -82
- package/dist/src/config/vector-cortex.js +25 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/heal/_acceptance-fixture.js +56 -0
- package/dist/src/vector-cortex/heal/_restore-fixture.js +96 -0
- package/dist/src/vector-cortex/heal/_vc6b-helpers.js +35 -0
- package/dist/src/vector-cortex/heal/closure-metrics.js +66 -0
- package/dist/src/vector-cortex/heal/closure-opt.js +242 -0
- package/dist/src/vector-cortex/heal/emit.js +69 -0
- package/dist/src/vector-cortex/heal/proof.js +210 -0
- package/dist/src/vector-cortex/heal/restore-emit.js +70 -0
- package/dist/src/vector-cortex/heal/restore-readers.js +114 -0
- package/dist/src/vector-cortex/heal/restore-types.js +66 -0
- package/dist/src/vector-cortex/heal/restore.js +140 -0
- package/dist/src/vector-cortex/heal/types.js +43 -0
- package/dist/src/vector-cortex/heal/verify.js +75 -0
- package/dist/vector-cortex/heal/_acceptance-fixture.js +56 -0
- package/dist/vector-cortex/heal/_restore-fixture.js +96 -0
- package/dist/vector-cortex/heal/_vc6b-helpers.js +35 -0
- package/dist/vector-cortex/heal/closure-metrics.js +66 -0
- package/dist/vector-cortex/heal/closure-opt.js +242 -0
- package/dist/vector-cortex/heal/emit.js +69 -0
- package/dist/vector-cortex/heal/proof.js +210 -0
- package/dist/vector-cortex/heal/restore-emit.js +70 -0
- package/dist/vector-cortex/heal/restore-readers.js +114 -0
- package/dist/vector-cortex/heal/restore-types.js +66 -0
- package/dist/vector-cortex/heal/restore.js +140 -0
- package/dist/vector-cortex/heal/types.js +43 -0
- package/dist/vector-cortex/heal/verify.js +75 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-DqTbaSRA.js → AreaChart-fxyq2KaH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-DqTbaSRA.js.map → AreaChart-fxyq2KaH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-BfNBeOvF.js → BarChart-D5EqAINP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-BfNBeOvF.js.map → BarChart-D5EqAINP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-C9SOMYB5.js → CacheTab-D4UWbzLo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-C9SOMYB5.js.map → CacheTab-D4UWbzLo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-GsMKP10_.js → EventsTab-BiXquGrX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-GsMKP10_.js.map → EventsTab-BiXquGrX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-DAOCXwRP.js → HealthTab-D5QvoTjy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-DAOCXwRP.js.map → HealthTab-D5QvoTjy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BtBeMkB5.js → MaintenanceTab-Cy_-CWgM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-BtBeMkB5.js.map → MaintenanceTab-Cy_-CWgM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DzndtgkZ.js → MemoryMapTab-DrAjgGOR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DzndtgkZ.js.map → MemoryMapTab-DrAjgGOR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DyxJ4eb8.js → MetricsTab-BvPHcuFN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DyxJ4eb8.js.map → MetricsTab-BvPHcuFN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DCiEdVWR.js → OverviewTab-DsbIigOQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DCiEdVWR.js.map → OverviewTab-DsbIigOQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-B5SIqVHA.js → ReposTab-7v_uZX_a.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-B5SIqVHA.js.map → ReposTab-7v_uZX_a.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DLWFJ9xI.js → SessionsTab-BXGrv-6C.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DLWFJ9xI.js.map → SessionsTab-BXGrv-6C.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-CvGXZyej.js → SetupTab-QRUXa65y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-CvGXZyej.js.map → SetupTab-QRUXa65y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-wZuQGxLS.js → TimeSavedCard-p327ZZKT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-wZuQGxLS.js.map → TimeSavedCard-p327ZZKT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-B7aOejch.js → TurnsTab-BcmBSlik.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-B7aOejch.js.map → TurnsTab-BcmBSlik.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-D2fjKNyT.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-DhWHjAtt.js → WikiTab-DLXFMuNy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-DhWHjAtt.js.map → WikiTab-DLXFMuNy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BrC-W87h.js → button-C9CghmRx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BrC-W87h.js.map → button-C9CghmRx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-0rgfRgr3.js → card-DWU-1y3t.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-0rgfRgr3.js.map → card-DWU-1y3t.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRresOQF.js → generateCategoricalChart-Dayy9dGn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-DRresOQF.js.map → generateCategoricalChart-Dayy9dGn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DohmtZWL.js → index-D9PsW3VZ.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DohmtZWL.js.map → index-D9PsW3VZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-Bs8g_zB5.js → switch-CnLFAmVT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-Bs8g_zB5.js.map → switch-CnLFAmVT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-DVVauIo8.js → toggle-HN5C7_vy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-DVVauIo8.js.map → toggle-HN5C7_vy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-DxH1PTAR.js → useSSE-DoJfk3Ce.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DxH1PTAR.js.map → useSSE-DoJfk3Ce.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/vector-cortex.ts +18 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexClosureCard.tsx +46 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexLedgerCard.tsx +80 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexRestoreCard.tsx +42 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +20 -164
- package/extensions/dashboard-client/src/tabs/VectorCortexTopologyCard.tsx +131 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +30 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-heal.ts +73 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +5 -0
- package/extensions/dashboard-server/route-dispatch.ts +114 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
- package/extensions/dashboard-server/routes-vector-cortex-heal.ts +103 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +6 -0
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +3 -86
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +27 -0
- package/src/config.ts +2 -0
- package/src/vector-cortex/heal/_acceptance-fixture.ts +108 -0
- package/src/vector-cortex/heal/_restore-fixture.ts +163 -0
- package/src/vector-cortex/heal/_vc6b-helpers.ts +55 -0
- package/src/vector-cortex/heal/closure-metrics.ts +73 -0
- package/src/vector-cortex/heal/closure-opt.ts +266 -0
- package/src/vector-cortex/heal/emit.ts +93 -0
- package/src/vector-cortex/heal/proof.ts +234 -0
- package/src/vector-cortex/heal/restore-emit.ts +94 -0
- package/src/vector-cortex/heal/restore-readers.ts +145 -0
- package/src/vector-cortex/heal/restore-types.ts +185 -0
- package/src/vector-cortex/heal/restore.ts +165 -0
- package/src/vector-cortex/heal/types.ts +248 -0
- package/src/vector-cortex/heal/verify.ts +93 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js.map +0 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/restore-readers.ts — the two EXACT source readers (VC6B).
|
|
3
|
+
*
|
|
4
|
+
* Split out of `restore.ts` so the orchestrator stays a short, readable policy
|
|
5
|
+
* file and each reader can be reasoned about on its own (PRACTICES: split at the
|
|
6
|
+
* 300-line soft limit, delegate-shell + impl).
|
|
7
|
+
*
|
|
8
|
+
* Both readers share one discipline: they return bytes ONLY after recomputing
|
|
9
|
+
* SHA-256 over the bytes they actually hold and comparing it to the digest the
|
|
10
|
+
* REQUEST pinned. Neither reader trusts the digest recorded alongside its own
|
|
11
|
+
* source — an `ExactShardV1.digest` is metadata that lives in the same file as
|
|
12
|
+
* the bytes, so a swapped file carries a matching (wrong) pair. Only the
|
|
13
|
+
* caller-supplied digest is authoritative, and only a fresh hash of the fetched
|
|
14
|
+
* bytes can be checked against it.
|
|
15
|
+
*
|
|
16
|
+
* Pure/deterministic/local: `node:crypto` only (a Node built-in, not a network
|
|
17
|
+
* call), no storage, no console (PREVENT-PI-004 / PREVENT-011).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
|
|
22
|
+
import type { EventV2, ExactShardV1, ShardRange } from "./restore-types.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 over bytes as LOWERCASE HEX with NO `sha256:` prefix — the
|
|
26
|
+
* `ExactShardV1.digest` / `ReconstructionSpan.digest` / `RestoreSpanRequest`
|
|
27
|
+
* convention. `EventV2.bytesDigest` prefixes this same value with `sha256:`.
|
|
28
|
+
*/
|
|
29
|
+
export function sha256Hex(bytes: Uint8Array): string {
|
|
30
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strip the `sha256:` prefix from an `EventV2.bytesDigest` so it can be compared
|
|
35
|
+
* against a bare hex digest. A value that does not carry the prefix is returned
|
|
36
|
+
* unchanged rather than mangled — the comparison then simply fails, which is the
|
|
37
|
+
* correct outcome for a malformed record.
|
|
38
|
+
*/
|
|
39
|
+
export function bareHex(digest: string): string {
|
|
40
|
+
return digest.startsWith("sha256:") ? digest.slice("sha256:".length) : digest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Structural range equality. All five components must match: two spans over the
|
|
45
|
+
* same seq window but different byte offsets are DIFFERENT spans (a re-encoded
|
|
46
|
+
* or re-offset stream is not the stream that was requested).
|
|
47
|
+
*/
|
|
48
|
+
export function rangeEquals(a: ShardRange, b: ShardRange): boolean {
|
|
49
|
+
return (
|
|
50
|
+
a.sessionId === b.sessionId &&
|
|
51
|
+
a.seqStart === b.seqStart &&
|
|
52
|
+
a.seqEnd === b.seqEnd &&
|
|
53
|
+
a.byteStart === b.byteStart &&
|
|
54
|
+
a.byteEnd === b.byteEnd
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What a reader attempt produced: bytes, a stated failure, or simply nothing. */
|
|
59
|
+
export type ReadOutcome =
|
|
60
|
+
/** Bytes fetched AND verified against the request digest. */
|
|
61
|
+
| { readonly kind: "bytes"; readonly bytes: Uint8Array }
|
|
62
|
+
/** A source existed but its bytes did not hash to the pinned digest. */
|
|
63
|
+
| { readonly kind: "digest-mismatch" }
|
|
64
|
+
/** No source of this tier covers the span; try the next tier. */
|
|
65
|
+
| { readonly kind: "absent" };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Mode A — indexed exact-shard read.
|
|
69
|
+
*
|
|
70
|
+
* Selection is by RANGE identity plus the shard's own recorded digest, which
|
|
71
|
+
* narrows the candidate set the way a real index lookup would. The accepted
|
|
72
|
+
* candidate is then re-hashed from its `originalBytes`: this second check is
|
|
73
|
+
* what catches a shard whose bytes were swapped AFTER the index lookup resolved
|
|
74
|
+
* (the sprint's unique failure injection). A candidate that matches by range but
|
|
75
|
+
* fails either digest check reports `digest-mismatch` — it does NOT silently fall
|
|
76
|
+
* through to the ledger, because a corrupt exact shard is a fact worth surfacing.
|
|
77
|
+
*/
|
|
78
|
+
export function readExactShard(
|
|
79
|
+
shards: readonly ExactShardV1[],
|
|
80
|
+
range: ShardRange,
|
|
81
|
+
digest: string,
|
|
82
|
+
): ReadOutcome {
|
|
83
|
+
const byRange = shards.filter((s) => rangeEquals(s.range, range));
|
|
84
|
+
if (byRange.length === 0) return { kind: "absent" };
|
|
85
|
+
|
|
86
|
+
const candidate = byRange.find((s) => s.digest === digest);
|
|
87
|
+
if (candidate === undefined) return { kind: "digest-mismatch" };
|
|
88
|
+
|
|
89
|
+
// Defense in depth: the shard's recorded digest is metadata, the bytes are the
|
|
90
|
+
// authority. Re-hash what we actually hold.
|
|
91
|
+
const bytes = candidate.originalBytes;
|
|
92
|
+
if (sha256Hex(bytes) !== digest) return { kind: "digest-mismatch" };
|
|
93
|
+
return { kind: "bytes", bytes };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Mode B — ledger range scan.
|
|
98
|
+
*
|
|
99
|
+
* An INDEPENDENT path from mode A: no shard index is consulted. The occurrence
|
|
100
|
+
* records covering `[seqStart..seqEnd]` are selected, sorted ascending by seq
|
|
101
|
+
* (the caller's array order is untrusted — a scan that concatenated records in
|
|
102
|
+
* arrival order would produce plausible-looking but wrong bytes), each record's
|
|
103
|
+
* own `bytesDigest` is verified, and the concatenation is hashed against the
|
|
104
|
+
* span digest.
|
|
105
|
+
*
|
|
106
|
+
* Both checks matter. Per-record verification localizes corruption to a single
|
|
107
|
+
* occurrence; the span-level hash catches a scan that is individually valid but
|
|
108
|
+
* collectively wrong — a missing record in the middle of the range, or a range
|
|
109
|
+
* that covers different content than the requester believed.
|
|
110
|
+
*/
|
|
111
|
+
export function readLedgerSpan(
|
|
112
|
+
events: readonly EventV2[],
|
|
113
|
+
range: ShardRange,
|
|
114
|
+
digest: string,
|
|
115
|
+
): ReadOutcome {
|
|
116
|
+
const covering = events
|
|
117
|
+
.filter(
|
|
118
|
+
(e) =>
|
|
119
|
+
e.sessionId === range.sessionId &&
|
|
120
|
+
e.seq >= range.seqStart &&
|
|
121
|
+
e.seq <= range.seqEnd,
|
|
122
|
+
)
|
|
123
|
+
.sort((a, b) => (a.seq < b.seq ? -1 : a.seq > b.seq ? 1 : 0));
|
|
124
|
+
|
|
125
|
+
if (covering.length === 0) return { kind: "absent" };
|
|
126
|
+
|
|
127
|
+
// Per-record byte authority (VC1A): a record whose own digest disagrees with
|
|
128
|
+
// its bytes is corrupt, and a corrupt record can never contribute to a restore.
|
|
129
|
+
for (const e of covering) {
|
|
130
|
+
if (sha256Hex(e.originalBytes) !== bareHex(e.bytesDigest)) {
|
|
131
|
+
return { kind: "digest-mismatch" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const total = covering.reduce((sum, e) => sum + e.originalBytes.length, 0);
|
|
136
|
+
const bytes = new Uint8Array(total);
|
|
137
|
+
let offset = 0;
|
|
138
|
+
for (const e of covering) {
|
|
139
|
+
bytes.set(e.originalBytes, offset);
|
|
140
|
+
offset += e.originalBytes.length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (sha256Hex(bytes) !== digest) return { kind: "digest-mismatch" };
|
|
144
|
+
return { kind: "bytes", bytes };
|
|
145
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/restore-types.ts — VC6B exact source restoration contract.
|
|
3
|
+
*
|
|
4
|
+
* VC6A optimized WHICH EDGES the closure plan walks. VC6B answers the next
|
|
5
|
+
* question: when the plan needs a node whose bytes are no longer in the live
|
|
6
|
+
* window, WHERE do those bytes come from? The answer is deliberately narrow —
|
|
7
|
+
* an EXACT source, or nothing.
|
|
8
|
+
*
|
|
9
|
+
* THE CARDINAL RULE. Restored bytes are ONLY ever read from an exact source:
|
|
10
|
+
* 1. an `ExactShardV1` whose range and digest both match the request, or
|
|
11
|
+
* 2. a scan of the `EventV2` occurrence ledger over the requested seq range.
|
|
12
|
+
* Bytes are NEVER inferred from an embedding, a semantic shard, a RAPTOR
|
|
13
|
+
* summary, or any other derived/lossy representation. A semantic tier can tell
|
|
14
|
+
* you what a span was ABOUT; it cannot tell you what the span WAS. Attempting to
|
|
15
|
+
* "restore" from a derived source would silently fabricate transcript history,
|
|
16
|
+
* so VC6B has no code path that can do it: `RestoreReader` exposes exactly the
|
|
17
|
+
* two exact sources and nothing else.
|
|
18
|
+
*
|
|
19
|
+
* THE VERIFICATION RULE. Every restored span must hash to the SHA-256 digest the
|
|
20
|
+
* REQUEST pinned, checked immediately before insertion. A source that matches by
|
|
21
|
+
* range but not by hash is rejected (`HEAL_RESTORE_DIGEST_MISMATCH`) — never
|
|
22
|
+
* "close enough". This is what makes a swapped shard file, a truncated read, or a
|
|
23
|
+
* corrupted ledger record fail loudly instead of poisoning the reconstruction.
|
|
24
|
+
*
|
|
25
|
+
* DIGEST PINNING (three fields exist; do not confuse them).
|
|
26
|
+
* - `ReconstructionSpan.digest` and `ExactShardV1.digest` are SHA-256 in
|
|
27
|
+
* LOWERCASE HEX with NO prefix.
|
|
28
|
+
* - `EventV2.bytesDigest` is `sha256:<hex>`, WITH the prefix.
|
|
29
|
+
* `RestoreSpanRequest.digest` works at SPAN level and uses the FORMER: bare
|
|
30
|
+
* lowercase hex, matching `ExactShardV1.digest` / `ReconstructionSpan.digest`.
|
|
31
|
+
* The prefixed `EventV2.bytesDigest` is used ONLY for per-event verification
|
|
32
|
+
* inside the ledger-scan path and is never the request-level digest. Mixing the
|
|
33
|
+
* two would make every ledger restoration fail (or, worse, make a prefix-stripped
|
|
34
|
+
* comparison accidentally succeed against the wrong granularity).
|
|
35
|
+
*
|
|
36
|
+
* BOUNDS. A restore request is attacker-shaped input: it names spans and byte
|
|
37
|
+
* ranges. `RESTORE_LIMIT_SPANS` / `RESTORE_LIMIT_BYTES` bound it BEFORE any
|
|
38
|
+
* reader is consulted, so an oversized request cannot be used to make the
|
|
39
|
+
* restorer walk the whole ledger (HEAL-LIMIT-002).
|
|
40
|
+
*
|
|
41
|
+
* Pure types + registered conformance IDs: no storage, no console, no network
|
|
42
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import type { ExactShardV1, ShardRange } from "../shards/types.js";
|
|
46
|
+
import type { EventV2 } from "../ledger/types.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One requested span: the node it restores, the byte/seq window that IDENTIFIES
|
|
50
|
+
* it, and the digest that AUTHENTICATES it.
|
|
51
|
+
*
|
|
52
|
+
* `range` is identity — it says which slice of the canonical stream to look for.
|
|
53
|
+
* `digest` is authentication — it says which bytes are acceptable. Both are
|
|
54
|
+
* required: identity alone would accept a shard file that was swapped in place,
|
|
55
|
+
* and a digest alone would require scanning every source.
|
|
56
|
+
*/
|
|
57
|
+
export interface RestoreSpanRequest {
|
|
58
|
+
/** The closure node these bytes belong to (echoed back in the result). */
|
|
59
|
+
readonly nodeId: string;
|
|
60
|
+
/** The span's identity: session + inclusive seq bounds + half-open bytes. */
|
|
61
|
+
readonly range: ShardRange;
|
|
62
|
+
/**
|
|
63
|
+
* SHA-256 of the span's original bytes, LOWERCASE HEX, NO `sha256:` prefix
|
|
64
|
+
* (matches `ExactShardV1.digest` / `ReconstructionSpan.digest`).
|
|
65
|
+
*/
|
|
66
|
+
readonly digest: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A batch of span restorations for one session. */
|
|
70
|
+
export interface RestoreRequestV1 {
|
|
71
|
+
readonly schema: "restore-request-v1";
|
|
72
|
+
readonly sessionId: string;
|
|
73
|
+
/** Requested spans, restored in this order (the result preserves it). */
|
|
74
|
+
readonly spans: readonly RestoreSpanRequest[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One successfully restored span. `source` records WHICH exact tier answered so
|
|
79
|
+
* the triad mode can be derived and so an operator can see whether the shard
|
|
80
|
+
* index is doing its job or every read is falling through to a ledger scan.
|
|
81
|
+
*/
|
|
82
|
+
export interface RestoreSpanResult {
|
|
83
|
+
readonly nodeId: string;
|
|
84
|
+
/** Which exact source produced the bytes (never a derived/semantic tier). */
|
|
85
|
+
readonly source: "exact-shard" | "ledger-scan";
|
|
86
|
+
/** The EXACT original bytes, verbatim — invalid UTF-8 included, unnormalized. */
|
|
87
|
+
readonly bytes: Uint8Array;
|
|
88
|
+
/** The verified SHA-256 (bare lowercase hex) — equals the request's digest. */
|
|
89
|
+
readonly digest: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The restoration outcome for one request.
|
|
94
|
+
*
|
|
95
|
+
* `mode` mirrors TRIAD_RESILIENCE:
|
|
96
|
+
* A — every span came from an indexed exact shard (the fast, normal path);
|
|
97
|
+
* B — every span was restored, but at least one required a ledger range scan
|
|
98
|
+
* (an INDEPENDENT code path: no shard index involved, bytes rebuilt by
|
|
99
|
+
* concatenating verified occurrence records);
|
|
100
|
+
* C — at least one span could not be restored from ANY exact source. Mode C
|
|
101
|
+
* OMITS the span and DISCLOSES the loss (`semanticLossStated`) rather than
|
|
102
|
+
* substituting derived text.
|
|
103
|
+
*/
|
|
104
|
+
export interface RestoreResultV1 {
|
|
105
|
+
readonly schema: "restore-result-v1";
|
|
106
|
+
readonly sessionId: string;
|
|
107
|
+
readonly mode: "A" | "B" | "C";
|
|
108
|
+
/** Restored spans in request order (only digest-verified spans appear here). */
|
|
109
|
+
readonly restored: readonly RestoreSpanResult[];
|
|
110
|
+
/** Node ids that could not be restored (identity only — never bytes). */
|
|
111
|
+
readonly missing: readonly string[];
|
|
112
|
+
/** Set in mode C: the caller MUST be told the old context is gone. */
|
|
113
|
+
readonly semanticLossStated: boolean;
|
|
114
|
+
/** Deduplicated failure codes in deterministic order. */
|
|
115
|
+
readonly codes: readonly RestoreFailureCode[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** VC6B failure codes (registered HEAL codes). */
|
|
119
|
+
export type RestoreFailureCode =
|
|
120
|
+
/** The request exceeds the span-count or aggregate-byte bound. */
|
|
121
|
+
| "HEAL_RESTORE_LIMIT"
|
|
122
|
+
/** A source's bytes do not hash to the pinned digest — nothing is inserted. */
|
|
123
|
+
| "HEAL_RESTORE_DIGEST_MISMATCH"
|
|
124
|
+
/** Neither an exact shard nor the ledger covers the requested span. */
|
|
125
|
+
| "HEAL_RESTORE_SOURCE_MISSING"
|
|
126
|
+
/** A restored span does not correspond to a requested span/digest. */
|
|
127
|
+
| "HEAL_RESTORE_RANGE_MISMATCH";
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Post-restoration verdict. `verifyRestored` re-derives every digest from the
|
|
131
|
+
* bytes actually carried in the result, so a result object that was mutated
|
|
132
|
+
* after `restoreSources` returned still fails before insertion.
|
|
133
|
+
*/
|
|
134
|
+
export type RestoreVerification =
|
|
135
|
+
| { readonly ok: true }
|
|
136
|
+
| { readonly ok: false; readonly codes: readonly RestoreFailureCode[] };
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The ONLY sources a restoration may read. Deliberately exhaustive: there is no
|
|
140
|
+
* embedding, semantic shard, or summary field here, so "restore from a derived
|
|
141
|
+
* source" is not merely forbidden by policy — it is unrepresentable.
|
|
142
|
+
*/
|
|
143
|
+
export interface RestoreReader {
|
|
144
|
+
/** Indexed exact shards (mode A). */
|
|
145
|
+
readonly exactShards: readonly ExactShardV1[];
|
|
146
|
+
/** Raw occurrence records for the ledger range scan (mode B). */
|
|
147
|
+
readonly ledgerEvents: readonly EventV2[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Maximum spans in one request. A request naming more spans than this is
|
|
152
|
+
* rejected outright — the bound exists so a single call cannot be turned into an
|
|
153
|
+
* unbounded traversal of the ledger.
|
|
154
|
+
*/
|
|
155
|
+
export const RESTORE_LIMIT_SPANS = 64;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Maximum aggregate requested bytes (4 MiB), summed from the REQUEST's ranges
|
|
159
|
+
* (`byteEnd - byteStart`) so the bound is enforceable without reading anything.
|
|
160
|
+
*/
|
|
161
|
+
export const RESTORE_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Registered VC6B conformance ID range (HEAL-016..030), continuing VC6A's
|
|
165
|
+
* HEAL-001..015. The acceptance test reads these rows from the v2 manifest and
|
|
166
|
+
* asserts each returns its manifest `ok`/`code`.
|
|
167
|
+
*/
|
|
168
|
+
export const RESTORE_IDS: readonly string[] = Array.from(
|
|
169
|
+
{ length: 15 },
|
|
170
|
+
(_v, i) => `HEAL-${String(i + 16).padStart(3, "0")}`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
/** Named VC6B conformance assertions (the sprint's headline rows). */
|
|
174
|
+
export const RESTORE_NAMED_IDS = [
|
|
175
|
+
"HEAL-SPAN-001",
|
|
176
|
+
"HEAL-LIMIT-002",
|
|
177
|
+
"HEAL-DIGEST-003",
|
|
178
|
+
] as const;
|
|
179
|
+
|
|
180
|
+
/** The two structured events the VC6B reporter emits. */
|
|
181
|
+
export type RestoreEventName =
|
|
182
|
+
| "vector_cortex_source_restored"
|
|
183
|
+
| "vector_cortex_restore_digest_rejected";
|
|
184
|
+
|
|
185
|
+
export type { EventV2, ExactShardV1, ShardRange };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/restore.ts — VC6B exact source restoration orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Turns a `RestoreRequestV1` into a `RestoreResultV1` by consulting, per span,
|
|
5
|
+
* the two EXACT sources in `restore-readers.ts` — an indexed exact shard first,
|
|
6
|
+
* then a ledger range scan — and inserting bytes only after they hash to the
|
|
7
|
+
* digest the request pinned.
|
|
8
|
+
*
|
|
9
|
+
* ORDER OF OPERATIONS IS THE CONTRACT.
|
|
10
|
+
*
|
|
11
|
+
* 1. BOUNDS, BEFORE ANY READER TOUCH. The span count and the aggregate
|
|
12
|
+
* requested byte span are computed PURELY FROM THE REQUEST (`byteEnd -
|
|
13
|
+
* byteStart`, which needs no source at all) and checked first. On breach we
|
|
14
|
+
* return immediately, having never read `reader.exactShards` or
|
|
15
|
+
* `reader.ledgerEvents`. This is HEAL-LIMIT-002: an oversized request must
|
|
16
|
+
* not be able to make the restorer walk the ledger even once, so the check
|
|
17
|
+
* cannot be "inside the loop, before the read" — it must be before the loop
|
|
18
|
+
* exists. The acceptance corpus proves it by passing EMPTY readers with an
|
|
19
|
+
* over-limit request: a reader-touching implementation would still return
|
|
20
|
+
* "missing" rather than "limit".
|
|
21
|
+
*
|
|
22
|
+
* 2. EXACT SHARD, then LEDGER, then MISSING. Sources are tried strongest-first.
|
|
23
|
+
* A digest mismatch anywhere is recorded and the span is NOT restored — the
|
|
24
|
+
* restorer never downgrades to "closest available bytes".
|
|
25
|
+
*
|
|
26
|
+
* 3. MODE from what actually happened, not from what was attempted. A = every
|
|
27
|
+
* span came from a shard; B = all restored, at least one via ledger scan;
|
|
28
|
+
* C = something is missing, and mode C STATES its semantic loss rather than
|
|
29
|
+
* hiding an incomplete restoration behind a successful-looking result.
|
|
30
|
+
*
|
|
31
|
+
* PURE. No storage, no console, no clock, no network — `node:crypto` (a Node
|
|
32
|
+
* built-in) is the only dependency beyond types (PREVENT-PI-004 / PREVENT-011).
|
|
33
|
+
* The reporter seam in `restore-emit.ts` is flag-gated; THIS arithmetic is not,
|
|
34
|
+
* so flag-off is byte-identical.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type {
|
|
38
|
+
RestoreFailureCode,
|
|
39
|
+
RestoreReader,
|
|
40
|
+
RestoreRequestV1,
|
|
41
|
+
RestoreResultV1,
|
|
42
|
+
RestoreSpanRequest,
|
|
43
|
+
RestoreSpanResult,
|
|
44
|
+
} from "./restore-types.js";
|
|
45
|
+
import { RESTORE_LIMIT_BYTES, RESTORE_LIMIT_SPANS } from "./restore-types.js";
|
|
46
|
+
import { readExactShard, readLedgerSpan } from "./restore-readers.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fixed code ordering so a result's `codes` array is deterministic regardless of
|
|
50
|
+
* which span failed first. Deterministic output is what lets the conformance
|
|
51
|
+
* corpus pin an exact expected value.
|
|
52
|
+
*/
|
|
53
|
+
const CODE_ORDER: readonly RestoreFailureCode[] = [
|
|
54
|
+
"HEAL_RESTORE_LIMIT",
|
|
55
|
+
"HEAL_RESTORE_DIGEST_MISMATCH",
|
|
56
|
+
"HEAL_RESTORE_SOURCE_MISSING",
|
|
57
|
+
"HEAL_RESTORE_RANGE_MISMATCH",
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
/** Deduplicate + sort codes into the fixed priority order. */
|
|
61
|
+
export function orderCodes(
|
|
62
|
+
codes: readonly RestoreFailureCode[],
|
|
63
|
+
): readonly RestoreFailureCode[] {
|
|
64
|
+
const seen = new Set(codes);
|
|
65
|
+
return CODE_ORDER.filter((c) => seen.has(c));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Total bytes the request ASKS for, derived from the ranges alone. Negative or
|
|
70
|
+
* inverted ranges contribute 0 rather than reducing the total — an inverted span
|
|
71
|
+
* must never be usable to smuggle a large request under the bound.
|
|
72
|
+
*/
|
|
73
|
+
function requestedBytes(spans: readonly RestoreSpanRequest[]): number {
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const s of spans) {
|
|
76
|
+
const len = s.range.byteEnd - s.range.byteStart;
|
|
77
|
+
total += len > 0 ? len : 0;
|
|
78
|
+
}
|
|
79
|
+
return total;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The immediate over-limit result: nothing read, everything missing. */
|
|
83
|
+
function limitExceeded(request: RestoreRequestV1): RestoreResultV1 {
|
|
84
|
+
return {
|
|
85
|
+
schema: "restore-result-v1",
|
|
86
|
+
sessionId: request.sessionId,
|
|
87
|
+
mode: "C",
|
|
88
|
+
restored: [],
|
|
89
|
+
missing: request.spans.map((s) => s.nodeId),
|
|
90
|
+
semanticLossStated: true,
|
|
91
|
+
codes: ["HEAL_RESTORE_LIMIT"],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Restore the exact original bytes for every requested span.
|
|
97
|
+
*
|
|
98
|
+
* Never throws: a request naming spans no source covers yields a mode-C result
|
|
99
|
+
* that discloses the loss, not an exception (PRACTICES: non-fatal — a failed
|
|
100
|
+
* restoration must degrade the prompt, never break the agent loop).
|
|
101
|
+
*/
|
|
102
|
+
export function restoreSources(
|
|
103
|
+
request: RestoreRequestV1,
|
|
104
|
+
reader: RestoreReader,
|
|
105
|
+
): RestoreResultV1 {
|
|
106
|
+
// (1) Bounds first — computed from the request only. `reader` is not touched
|
|
107
|
+
// on this path, which is exactly what HEAL-LIMIT-002 asserts.
|
|
108
|
+
if (
|
|
109
|
+
request.spans.length > RESTORE_LIMIT_SPANS ||
|
|
110
|
+
requestedBytes(request.spans) > RESTORE_LIMIT_BYTES
|
|
111
|
+
) {
|
|
112
|
+
return limitExceeded(request);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const restored: RestoreSpanResult[] = [];
|
|
116
|
+
const missing: string[] = [];
|
|
117
|
+
const codes: RestoreFailureCode[] = [];
|
|
118
|
+
let usedLedger = false;
|
|
119
|
+
|
|
120
|
+
// (2) Per span, strongest source first, in request order.
|
|
121
|
+
for (const span of request.spans) {
|
|
122
|
+
const exact = readExactShard(reader.exactShards, span.range, span.digest);
|
|
123
|
+
if (exact.kind === "bytes") {
|
|
124
|
+
restored.push({
|
|
125
|
+
nodeId: span.nodeId,
|
|
126
|
+
source: "exact-shard",
|
|
127
|
+
bytes: exact.bytes,
|
|
128
|
+
digest: span.digest,
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (exact.kind === "digest-mismatch") codes.push("HEAL_RESTORE_DIGEST_MISMATCH");
|
|
133
|
+
|
|
134
|
+
const ledger = readLedgerSpan(reader.ledgerEvents, span.range, span.digest);
|
|
135
|
+
if (ledger.kind === "bytes") {
|
|
136
|
+
usedLedger = true;
|
|
137
|
+
restored.push({
|
|
138
|
+
nodeId: span.nodeId,
|
|
139
|
+
source: "ledger-scan",
|
|
140
|
+
bytes: ledger.bytes,
|
|
141
|
+
digest: span.digest,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ledger.kind === "digest-mismatch") codes.push("HEAL_RESTORE_DIGEST_MISMATCH");
|
|
146
|
+
|
|
147
|
+
// (3) Neither exact source answered. Identity only — never bytes.
|
|
148
|
+
missing.push(span.nodeId);
|
|
149
|
+
codes.push("HEAL_RESTORE_SOURCE_MISSING");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const complete = missing.length === 0 && restored.length === request.spans.length;
|
|
153
|
+
const mode: RestoreResultV1["mode"] = !complete ? "C" : usedLedger ? "B" : "A";
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
schema: "restore-result-v1",
|
|
157
|
+
sessionId: request.sessionId,
|
|
158
|
+
mode,
|
|
159
|
+
restored,
|
|
160
|
+
missing,
|
|
161
|
+
// Mode C omits spans, so it MUST disclose that the old context is gone.
|
|
162
|
+
semanticLossStated: mode === "C",
|
|
163
|
+
codes: orderCodes(codes),
|
|
164
|
+
};
|
|
165
|
+
}
|