pi-mega-compact 0.20.8 → 0.20.10
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 +29 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/route-dispatch.js +8 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-vector-cortex.js +2 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-heal.js +42 -3
- package/dist/extensions/dashboard-server/routes-vector-cortex-repair.js +63 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +3 -1
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/src/config/vector-cortex.js +29 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/heal/_repair-fixture.js +70 -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/controller.js +165 -0
- package/dist/src/vector-cortex/heal/rebuild.js +136 -0
- package/dist/src/vector-cortex/heal/repair-emit.js +83 -0
- package/dist/src/vector-cortex/heal/repair-types.js +74 -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 +2 -0
- package/dist/src/vector-cortex/heal/verify.js +75 -0
- package/dist/vector-cortex/heal/_repair-fixture.js +70 -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/controller.js +165 -0
- package/dist/vector-cortex/heal/rebuild.js +136 -0
- package/dist/vector-cortex/heal/repair-emit.js +83 -0
- package/dist/vector-cortex/heal/repair-types.js +74 -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 +2 -0
- package/dist/vector-cortex/heal/verify.js +75 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-5yxaI8IO.js → AreaChart-BDMjyRQp.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-5yxaI8IO.js.map → AreaChart-BDMjyRQp.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-BoHhCqH-.js → BarChart-CqzPzOkj.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-BoHhCqH-.js.map → BarChart-CqzPzOkj.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-Di6L3CsF.js → CacheTab-Bo8SH9q8.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-Di6L3CsF.js.map → CacheTab-Bo8SH9q8.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-Cdff3h6s.js → EventsTab-DIEs6zC-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-Cdff3h6s.js.map → EventsTab-DIEs6zC-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-MthYYVLw.js → HealthTab-3a8IjwIK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-MthYYVLw.js.map → HealthTab-3a8IjwIK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D9G4ntsG.js → MaintenanceTab-CdKwAcXo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D9G4ntsG.js.map → MaintenanceTab-CdKwAcXo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CN8Lr5Qf.js → MemoryMapTab-D2hHuNj-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-CN8Lr5Qf.js.map → MemoryMapTab-D2hHuNj-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CgO32kSo.js → MetricsTab-CFhIhqrr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CgO32kSo.js.map → MetricsTab-CFhIhqrr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-Cb4kR0h1.js → OverviewTab-Dzp5ZoiA.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-Cb4kR0h1.js.map → OverviewTab-Dzp5ZoiA.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-Dt1FGnMR.js → ReposTab-CFJT9mDR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-Dt1FGnMR.js.map → ReposTab-CFJT9mDR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-D7D2RW6U.js → SessionsTab-BhP1z6_y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-D7D2RW6U.js.map → SessionsTab-BhP1z6_y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-CgV5xCzz.js → SetupTab-2Glh7MKk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-CgV5xCzz.js.map → SetupTab-2Glh7MKk.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CCNuJjyO.js → TimeSavedCard-DmwBDhkw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CCNuJjyO.js.map → TimeSavedCard-DmwBDhkw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Cv1OCttJ.js → TurnsTab-CYIaOUUZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Cv1OCttJ.js.map → TurnsTab-CYIaOUUZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD7vGaRS.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DD7vGaRS.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-B48QnQrK.js → WikiTab-CP-JEd17.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-B48QnQrK.js.map → WikiTab-CP-JEd17.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BBQl2MiF.js → button-B1RhLsGs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BBQl2MiF.js.map → button-B1RhLsGs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-BYbefUur.js → card-BLPT2-8G.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-BYbefUur.js.map → card-BLPT2-8G.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-swm0YZe0.js → generateCategoricalChart-DVOMJzL2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-swm0YZe0.js.map → generateCategoricalChart-DVOMJzL2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DeIky7xm.js → index-CuLdiHRl.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DeIky7xm.js.map → index-CuLdiHRl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-CctU78ww.js → switch-C5tqzhcl.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-CctU78ww.js.map → switch-C5tqzhcl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-BRCNCoiI.js → toggle-HcM6W2Yl.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-BRCNCoiI.js.map → toggle-HcM6W2Yl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-WfOiWDH_.js → useSSE-BtWUs2kL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-WfOiWDH_.js.map → useSSE-BtWUs2kL.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/VectorCortexRepairCard.tsx +43 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexRestoreCard.tsx +42 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexShardsCard.tsx +89 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +22 -60
- package/extensions/dashboard-client/src/types/vector-cortex.ts +33 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex-heal.ts +68 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +5 -1
- package/extensions/dashboard-server/route-dispatch.ts +6 -0
- package/extensions/dashboard-server/routes-rag-settings-vector-cortex.ts +12 -0
- package/extensions/dashboard-server/routes-vector-cortex-heal.ts +51 -4
- package/extensions/dashboard-server/routes-vector-cortex-repair.ts +72 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +6 -1
- package/extensions/dashboard-server/routes.ts +1 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +31 -0
- package/src/config.ts +2 -0
- package/src/vector-cortex/heal/_repair-fixture.ts +138 -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/controller.ts +189 -0
- package/src/vector-cortex/heal/rebuild.ts +200 -0
- package/src/vector-cortex/heal/repair-emit.ts +118 -0
- package/src/vector-cortex/heal/repair-types.ts +227 -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 +50 -0
- package/src/vector-cortex/heal/verify.ts +93 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DTabQo6h.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DTabQo6h.js.map +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/rebuild.ts — VC6C generation rebuild + atomic pointer switch.
|
|
3
|
+
*
|
|
4
|
+
* Executes what `controller.ts` planned: materialize a NEW generation, verify its
|
|
5
|
+
* root digest, and only then flip the live pointer.
|
|
6
|
+
*
|
|
7
|
+
* COPY, VERIFY, SWITCH — in that order, always.
|
|
8
|
+
*
|
|
9
|
+
* 1. COPY. The rebuild writes into `plan.generation` (always `current + 1`),
|
|
10
|
+
* never into the live generation. The thing currently being served is never
|
|
11
|
+
* the thing being modified, so a crash mid-rebuild leaves a complete old
|
|
12
|
+
* generation and a partial new one — never a torn live one.
|
|
13
|
+
* 2. VERIFY. The new generation's root digest must equal the digest the plan
|
|
14
|
+
* pinned. This is the ONLY gate on the pointer.
|
|
15
|
+
* 3. SWITCH. `switchPointer` moves the pointer iff verification passed.
|
|
16
|
+
*
|
|
17
|
+
* A FAILED VERIFICATION DELETES NO EVIDENCE. On mismatch we keep the old pointer
|
|
18
|
+
* AND leave the new generation on disk. That is deliberate: the corrupt
|
|
19
|
+
* generation is the only artifact that can explain WHY the rebuild was wrong, and
|
|
20
|
+
* a self-healing system that tidies up its failures is a system that cannot be
|
|
21
|
+
* debugged. Cleanup is a separate, explicit operator action.
|
|
22
|
+
*
|
|
23
|
+
* CRASH SAFETY IS A CONSEQUENCE OF THE ORDER, NOT AN EXTRA STEP. The pointer is
|
|
24
|
+
* the single atomic commit point. Kill the process after step 1 or 2 and the old
|
|
25
|
+
* pointer is still live, so the next start serves the prior generation and simply
|
|
26
|
+
* re-plans — the orphaned generation is inert. This is what
|
|
27
|
+
* `rebuild-chaos.test.ts` pins.
|
|
28
|
+
*
|
|
29
|
+
* THE TRIAD (independent algorithms, per TRIAD_RESILIENCE).
|
|
30
|
+
* A — TARGETED: rebuild only `plan.range`, reusing the prior generation for
|
|
31
|
+
* everything outside it. Cheap; needs a healthy prior generation.
|
|
32
|
+
* B — FULL DETERMINISTIC: re-derive the whole subsystem from the byte ledger,
|
|
33
|
+
* reusing NOTHING. Independent of A: it shares no index, no prior
|
|
34
|
+
* generation, and no incremental state, so a bug or corruption that breaks
|
|
35
|
+
* A cannot break B the same way.
|
|
36
|
+
* C — DISABLE DERIVED STATE: no rebuild at all. Mode C is a real outcome, not
|
|
37
|
+
* an error path, and it MUST state its loss of old semantic context — the
|
|
38
|
+
* subsystem serves nothing rather than serving something wrong.
|
|
39
|
+
*
|
|
40
|
+
* PURE. `node:crypto` only — no storage, no console, no clock, no network
|
|
41
|
+
* (PREVENT-PI-004 / PREVENT-011). Callers own persistence; this module owns the
|
|
42
|
+
* decision and the digest arithmetic, which is what makes it fixture-testable.
|
|
43
|
+
*/
|
|
44
|
+
import { createHash } from "node:crypto";
|
|
45
|
+
/** Root digest of a generation's bytes: SHA-256, bare lowercase hex. */
|
|
46
|
+
export function rootDigest(bytes) {
|
|
47
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Materialize + verify one generation.
|
|
51
|
+
*
|
|
52
|
+
* Hashes the rebuilt bytes and compares to the pinned root digest. A mismatch
|
|
53
|
+
* returns `HEAL_REPAIR_DIGEST_MISMATCH` and — critically — the caller must NOT
|
|
54
|
+
* switch the pointer; `switchPointer` enforces that structurally by requiring the
|
|
55
|
+
* verified flag.
|
|
56
|
+
*
|
|
57
|
+
* An EMPTY rebuild is a failure, not an empty success: a generation with no bytes
|
|
58
|
+
* would hash to the digest of nothing, and if a plan ever pinned that digest an
|
|
59
|
+
* empty rebuild would "verify" and the pointer would flip to nothing at all. It
|
|
60
|
+
* is reported as `HEAL_REBUILD_FAILED` because the rebuild produced no artifact.
|
|
61
|
+
*/
|
|
62
|
+
export function rebuildGeneration(input, mode = "A") {
|
|
63
|
+
if (input.sourceBytes.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
code: "HEAL_REBUILD_FAILED",
|
|
67
|
+
generation: input.generation,
|
|
68
|
+
mode,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const digest = rootDigest(input.sourceBytes);
|
|
72
|
+
if (digest !== input.expectedDigest) {
|
|
73
|
+
// Evidence retained: the caller keeps the generation on disk for inspection.
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
code: "HEAL_REPAIR_DIGEST_MISMATCH",
|
|
77
|
+
generation: input.generation,
|
|
78
|
+
mode,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, generation: input.generation, digest, mode };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The atomic commit point: flip the live pointer iff the new generation verified.
|
|
85
|
+
*
|
|
86
|
+
* `verified` is a required argument rather than something re-derived here, so a
|
|
87
|
+
* caller cannot switch the pointer without having gone through
|
|
88
|
+
* `rebuildGeneration` — "switch without verifying" is not expressible.
|
|
89
|
+
*
|
|
90
|
+
* A non-monotonic switch is also refused: the new generation must be strictly
|
|
91
|
+
* greater than the current one. Replaying a stale plan after a restart would
|
|
92
|
+
* otherwise roll the pointer BACKWARDS onto an older generation, silently
|
|
93
|
+
* un-healing the subsystem.
|
|
94
|
+
*/
|
|
95
|
+
export function switchPointer(currentGen, newGen, verified) {
|
|
96
|
+
if (!verified)
|
|
97
|
+
return { switched: false, generation: currentGen };
|
|
98
|
+
if (newGen <= currentGen)
|
|
99
|
+
return { switched: false, generation: currentGen };
|
|
100
|
+
return { switched: true, generation: newGen };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Apply the triad arm for a rebuild.
|
|
104
|
+
*
|
|
105
|
+
* A and B run the same verification (a digest is a digest) but are reached by
|
|
106
|
+
* INDEPENDENT production paths: A reuses the prior generation and rebuilds only
|
|
107
|
+
* the planned range, while B re-derives everything from the byte ledger sharing
|
|
108
|
+
* no index or incremental state with A. C performs no rebuild at all and states
|
|
109
|
+
* its loss.
|
|
110
|
+
*/
|
|
111
|
+
export function applyTriad(mode, input) {
|
|
112
|
+
if (mode === "C") {
|
|
113
|
+
// Derived state disabled: no rebuild, and the loss of old semantic context
|
|
114
|
+
// is DISCLOSED rather than papered over with a stale or partial generation.
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
code: "HEAL_REBUILD_FAILED",
|
|
118
|
+
generation: input.generation,
|
|
119
|
+
mode: "C",
|
|
120
|
+
semanticLossStated: true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return rebuildGeneration(input, mode);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Convenience: rebuild then switch, returning both halves.
|
|
127
|
+
*
|
|
128
|
+
* The pointer moves only on a verified rebuild, so a failed verification yields
|
|
129
|
+
* `switched:false` with the ORIGINAL generation still live and the new (corrupt)
|
|
130
|
+
* generation left intact on disk for inspection.
|
|
131
|
+
*/
|
|
132
|
+
export function rebuildAndSwitch(input, currentGen, mode = "A") {
|
|
133
|
+
const result = applyTriad(mode, input);
|
|
134
|
+
const pointer = switchPointer(currentGen, input.generation, result.ok);
|
|
135
|
+
return { result, pointer };
|
|
136
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/repair-emit.ts — VC6C event reporter seam.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `./restore-emit.ts`: a thin `safe()` wrapper around an optional
|
|
5
|
+
* injected `emit`, and the three event names the sprint spec requires verbatim:
|
|
6
|
+
* - `vector_cortex_repair_planned` — a gap was detected, rebuild queued.
|
|
7
|
+
* - `vector_cortex_repair_pointer_switched`— a verified generation went live.
|
|
8
|
+
* - `vector_cortex_repair_backoff` — suppressed (rate limit) or failed.
|
|
9
|
+
*
|
|
10
|
+
* FLAG SEMANTICS. `detectGaps` / `planRebuild` / `rebuildGeneration` /
|
|
11
|
+
* `switchPointer` are PURE arithmetic and run REGARDLESS of `MEGACOMPACT_VC6C`.
|
|
12
|
+
* The flag gates ONLY this reporting + dashboard seam: with the flag off we still
|
|
13
|
+
* detect gaps, still verify digests, and still refuse unverified pointer
|
|
14
|
+
* switches — we just do not announce it under the VC6C event namespace. That is
|
|
15
|
+
* what makes flag-off byte-identical to VC6B: the computation is never skipped,
|
|
16
|
+
* only the emission.
|
|
17
|
+
*
|
|
18
|
+
* PAYLOAD DISCIPLINE. These events carry the SUBSYSTEM NAME, generation numbers,
|
|
19
|
+
* timings, and codes — never rebuilt bytes, never a root digest of user content,
|
|
20
|
+
* never a seq range's contents. The subsystem name is an operator-facing
|
|
21
|
+
* identifier ("topology"), not user data. A repair event is exactly the place
|
|
22
|
+
* where an unguarded `payload` field would leak a rebuilt transcript into a log.
|
|
23
|
+
*
|
|
24
|
+
* No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
|
|
25
|
+
* is a structured JSON event with `ts` + `event`.
|
|
26
|
+
*/
|
|
27
|
+
import { VC6C_ENABLED } from "../../config/vector-cortex.js";
|
|
28
|
+
/** Run `fn` only when an emit exists; a reporting failure is never fatal. */
|
|
29
|
+
function safe(emit, fn) {
|
|
30
|
+
if (emit === undefined)
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
fn(emit);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Non-fatal: a reporting failure must never break the agent loop.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The event names VC6C emits, exported for the dashboard seam and tests. */
|
|
40
|
+
export const REPAIR_EVENT_NAMES = [
|
|
41
|
+
"vector_cortex_repair_planned",
|
|
42
|
+
"vector_cortex_repair_pointer_switched",
|
|
43
|
+
"vector_cortex_repair_backoff",
|
|
44
|
+
];
|
|
45
|
+
/** Report a planned rebuild: which subsystem, which generation, what delay. */
|
|
46
|
+
export function reportRepairPlanned(emit, payload) {
|
|
47
|
+
if (!VC6C_ENABLED())
|
|
48
|
+
return;
|
|
49
|
+
safe(emit, (e) => e("vector_cortex_repair_planned", {
|
|
50
|
+
ts: undefined,
|
|
51
|
+
event: "vector_cortex_repair_planned",
|
|
52
|
+
subsystem: payload.subsystem,
|
|
53
|
+
generation: payload.generation,
|
|
54
|
+
backoffMs: payload.backoffMs,
|
|
55
|
+
gapSize: payload.gapSize,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
/** Report the atomic commit: a verified generation became live. */
|
|
59
|
+
export function reportRepairPointerSwitched(emit, payload) {
|
|
60
|
+
if (!VC6C_ENABLED())
|
|
61
|
+
return;
|
|
62
|
+
safe(emit, (e) => e("vector_cortex_repair_pointer_switched", {
|
|
63
|
+
ts: undefined,
|
|
64
|
+
event: "vector_cortex_repair_pointer_switched",
|
|
65
|
+
subsystem: payload.subsystem,
|
|
66
|
+
fromGeneration: payload.fromGeneration,
|
|
67
|
+
toGeneration: payload.toGeneration,
|
|
68
|
+
mode: payload.mode,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
/** Report a suppressed or failed rebuild now waiting out its backoff. */
|
|
72
|
+
export function reportRepairBackoff(emit, payload) {
|
|
73
|
+
if (!VC6C_ENABLED())
|
|
74
|
+
return;
|
|
75
|
+
safe(emit, (e) => e("vector_cortex_repair_backoff", {
|
|
76
|
+
ts: undefined,
|
|
77
|
+
event: "vector_cortex_repair_backoff",
|
|
78
|
+
subsystem: payload.subsystem,
|
|
79
|
+
code: payload.code,
|
|
80
|
+
backoffMs: payload.backoffMs,
|
|
81
|
+
attempt: payload.attempt,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/repair-types.ts — VC6C self-healing derived controller.
|
|
3
|
+
*
|
|
4
|
+
* VC6B answered "when a node's bytes are gone, WHERE do they come from?". VC6C
|
|
5
|
+
* answers the question one level up: when a DERIVED subsystem (topology, shards,
|
|
6
|
+
* closure) has fallen BEHIND the durable authority, how do we notice, and how do
|
|
7
|
+
* we catch it up without ever risking the authority itself?
|
|
8
|
+
*
|
|
9
|
+
* THE AUTHORITY IS READ-ONLY, ALWAYS. The controller compares each derived
|
|
10
|
+
* source's high-water to the durable authority high-water and plans work. It has
|
|
11
|
+
* no write path to the authority — not a guarded one, not an admin one. Derived
|
|
12
|
+
* state is disposable and can always be rebuilt from the byte ledger; the
|
|
13
|
+
* authority is not, so the repair path is deliberately one-directional. This is
|
|
14
|
+
* why `RepairState` carries `authorityHighWater` as a plain readonly field and
|
|
15
|
+
* why no function in `controller.ts` returns anything that could be applied to it.
|
|
16
|
+
*
|
|
17
|
+
* NEVER READ PAST THE AUTHORITY (TRIAD_RESILIENCE §frontier). A derived builder
|
|
18
|
+
* may not read beyond the durable CONTIGUOUS authority high-water. During an
|
|
19
|
+
* authority outage that high-water FREEZES even though the spool keeps accepting
|
|
20
|
+
* frames — so a derived subsystem that is "behind" a frozen frontier is CORRECT,
|
|
21
|
+
* not broken, and planning a rebuild against the spool tail would materialize
|
|
22
|
+
* frames that are not yet durable. `detectGaps` therefore treats
|
|
23
|
+
* `authorityFrozen` as a hard stop (`HEAL_REPAIR_AUTHORITY_FROZEN`) rather than
|
|
24
|
+
* as a large gap to chase. After the drain, catch-up resumes from the OLD
|
|
25
|
+
* high-water; it never jumps to the tail.
|
|
26
|
+
*
|
|
27
|
+
* REBUILD IS COPY-THEN-SWITCH, NEVER IN-PLACE. `rebuild.ts` materializes a NEW
|
|
28
|
+
* generation, verifies its root digest, and only then flips the pointer. A failed
|
|
29
|
+
* verification keeps the old pointer and DELETES NO EVIDENCE: the corrupt
|
|
30
|
+
* generation stays on disk to be inspected. An in-place repair would, by
|
|
31
|
+
* construction, destroy the only copy of the thing that was about to be proven
|
|
32
|
+
* wrong.
|
|
33
|
+
*
|
|
34
|
+
* RATE LIMIT + BACKOFF ARE THE BLAST RADIUS. A subsystem that fails to rebuild
|
|
35
|
+
* will fail again, usually instantly, so an unbounded retry loop turns one broken
|
|
36
|
+
* shard into a CPU-saturating rebuild storm. One rebuild per subsystem per 5
|
|
37
|
+
* minutes bounds the steady state, and the deterministic exponential backoff
|
|
38
|
+
* (30s * 2^attempt, capped at 15 min, ±10% jitter derived from the SUBSYSTEM
|
|
39
|
+
* DIGEST rather than `Math.random`) bounds the failure state while keeping the
|
|
40
|
+
* schedule reproducible in a fixture.
|
|
41
|
+
*
|
|
42
|
+
* Pure types + registered conformance IDs: no storage, no console, no clock, no
|
|
43
|
+
* network (PREVENT-PI-004 / PREVENT-011).
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* One rebuild per subsystem per 5 minutes. Bounds a rebuild storm: a subsystem
|
|
47
|
+
* that fails will fail again immediately, and without this an unhealthy tier
|
|
48
|
+
* would saturate the box re-materializing the same broken generation.
|
|
49
|
+
*/
|
|
50
|
+
export const REPAIR_RATE_LIMIT_MS = 5 * 60_000;
|
|
51
|
+
/**
|
|
52
|
+
* Exponential backoff base/cap, matching the breaker's retry rule
|
|
53
|
+
* (TRIAD_RESILIENCE): `30s * 2^attempt`, capped at 15 minutes.
|
|
54
|
+
*/
|
|
55
|
+
export const REPAIR_BACKOFF_BASE_MS = 30_000;
|
|
56
|
+
export const REPAIR_BACKOFF_CAP_MS = 15 * 60_000;
|
|
57
|
+
/**
|
|
58
|
+
* ±10% deterministic jitter. Derived from the SUBSYSTEM DIGEST, not a PRNG, so
|
|
59
|
+
* two subsystems desynchronize (no thundering herd) while any single subsystem's
|
|
60
|
+
* schedule stays reproducible in a fixture.
|
|
61
|
+
*/
|
|
62
|
+
export const REPAIR_BACKOFF_JITTER = 0.1;
|
|
63
|
+
/**
|
|
64
|
+
* Registered VC6C conformance ID range (HEAL-031..045), continuing VC6B's
|
|
65
|
+
* HEAL-016..030. The acceptance test reads these rows from the v2 manifest and
|
|
66
|
+
* asserts each returns its manifest `ok`/`code`.
|
|
67
|
+
*/
|
|
68
|
+
export const REPAIR_IDS = Array.from({ length: 15 }, (_v, i) => `HEAL-${String(i + 31).padStart(3, "0")}`);
|
|
69
|
+
/** Named VC6C conformance assertions (the sprint's headline rows). */
|
|
70
|
+
export const REPAIR_NAMED_IDS = [
|
|
71
|
+
"HEAL-GAP-001",
|
|
72
|
+
"HEAL-RATE-002",
|
|
73
|
+
"HEAL-SWITCH-003",
|
|
74
|
+
];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/heal/restore-emit.ts — VC6B event reporter seam.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `./emit.ts`: a thin `safe()` wrapper around an optional injected
|
|
5
|
+
* `emit` (unit tests pass `undefined` and stay pure), and the two event names the
|
|
6
|
+
* sprint spec requires verbatim:
|
|
7
|
+
* - `vector_cortex_source_restored` — a restore batch completed.
|
|
8
|
+
* - `vector_cortex_restore_digest_rejected` — a source failed its digest check.
|
|
9
|
+
*
|
|
10
|
+
* FLAG SEMANTICS. `restoreSources` / `verifyRestored` are PURE arithmetic and run
|
|
11
|
+
* REGARDLESS of `MEGACOMPACT_VC6B`. The flag gates ONLY this reporting +
|
|
12
|
+
* dashboard seam: with the flag off we still restore and still verify, we just do
|
|
13
|
+
* not announce it under the VC6B event namespace. That is what makes flag-off
|
|
14
|
+
* byte-identical to VC6A — the computation is never skipped, only the emission.
|
|
15
|
+
*
|
|
16
|
+
* PAYLOAD DISCIPLINE. These events carry COUNTS and MODES only — never restored
|
|
17
|
+
* bytes, never node text, never a digest of user content. The exact ledger is not
|
|
18
|
+
* diagnostic data (SECURITY_PRIVACY), and a restoration event is exactly the
|
|
19
|
+
* place where an unguarded `payload` field would leak the entire transcript into
|
|
20
|
+
* a log file.
|
|
21
|
+
*
|
|
22
|
+
* No console, no storage, no network (PREVENT-PI-004 / PREVENT-011). Every line
|
|
23
|
+
* is a structured JSON event with `ts` + `event`.
|
|
24
|
+
*/
|
|
25
|
+
import { VC6B_ENABLED } from "../../config/vector-cortex.js";
|
|
26
|
+
/** Run `fn` only when an emit exists; a reporting failure is never fatal. */
|
|
27
|
+
function safe(emit, fn) {
|
|
28
|
+
if (emit === undefined)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
fn(emit);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Non-fatal: a reporting failure must never break the agent loop.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** The event names VC6B emits, exported for the dashboard seam and tests. */
|
|
38
|
+
export const RESTORE_EVENT_NAMES = [
|
|
39
|
+
"vector_cortex_source_restored",
|
|
40
|
+
"vector_cortex_restore_digest_rejected",
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Report a completed restoration. Counts and mode only — enough to see whether
|
|
44
|
+
* the shard index is serving reads (mode A) or every span is falling through to
|
|
45
|
+
* a ledger scan (mode B), without disclosing what was restored.
|
|
46
|
+
*/
|
|
47
|
+
export function reportSourceRestored(emit, payload) {
|
|
48
|
+
if (!VC6B_ENABLED())
|
|
49
|
+
return;
|
|
50
|
+
safe(emit, (e) => e("vector_cortex_source_restored", {
|
|
51
|
+
ts: undefined,
|
|
52
|
+
event: "vector_cortex_source_restored",
|
|
53
|
+
sessionId: payload.sessionId,
|
|
54
|
+
restoredCount: payload.restoredCount,
|
|
55
|
+
missingCount: payload.missingCount,
|
|
56
|
+
mode: payload.mode,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/** Report a rejected source (digest/limit/range). Suppressed under flag-off. */
|
|
60
|
+
export function reportRestoreDigestRejected(emit, payload) {
|
|
61
|
+
if (!VC6B_ENABLED())
|
|
62
|
+
return;
|
|
63
|
+
safe(emit, (e) => e("vector_cortex_restore_digest_rejected", {
|
|
64
|
+
ts: undefined,
|
|
65
|
+
event: "vector_cortex_restore_digest_rejected",
|
|
66
|
+
sessionId: payload.sessionId,
|
|
67
|
+
code: payload.code,
|
|
68
|
+
mode: payload.mode,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
/**
|
|
21
|
+
* SHA-256 over bytes as LOWERCASE HEX with NO `sha256:` prefix — the
|
|
22
|
+
* `ExactShardV1.digest` / `ReconstructionSpan.digest` / `RestoreSpanRequest`
|
|
23
|
+
* convention. `EventV2.bytesDigest` prefixes this same value with `sha256:`.
|
|
24
|
+
*/
|
|
25
|
+
export function sha256Hex(bytes) {
|
|
26
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Strip the `sha256:` prefix from an `EventV2.bytesDigest` so it can be compared
|
|
30
|
+
* against a bare hex digest. A value that does not carry the prefix is returned
|
|
31
|
+
* unchanged rather than mangled — the comparison then simply fails, which is the
|
|
32
|
+
* correct outcome for a malformed record.
|
|
33
|
+
*/
|
|
34
|
+
export function bareHex(digest) {
|
|
35
|
+
return digest.startsWith("sha256:") ? digest.slice("sha256:".length) : digest;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Structural range equality. All five components must match: two spans over the
|
|
39
|
+
* same seq window but different byte offsets are DIFFERENT spans (a re-encoded
|
|
40
|
+
* or re-offset stream is not the stream that was requested).
|
|
41
|
+
*/
|
|
42
|
+
export function rangeEquals(a, b) {
|
|
43
|
+
return (a.sessionId === b.sessionId &&
|
|
44
|
+
a.seqStart === b.seqStart &&
|
|
45
|
+
a.seqEnd === b.seqEnd &&
|
|
46
|
+
a.byteStart === b.byteStart &&
|
|
47
|
+
a.byteEnd === b.byteEnd);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mode A — indexed exact-shard read.
|
|
51
|
+
*
|
|
52
|
+
* Selection is by RANGE identity plus the shard's own recorded digest, which
|
|
53
|
+
* narrows the candidate set the way a real index lookup would. The accepted
|
|
54
|
+
* candidate is then re-hashed from its `originalBytes`: this second check is
|
|
55
|
+
* what catches a shard whose bytes were swapped AFTER the index lookup resolved
|
|
56
|
+
* (the sprint's unique failure injection). A candidate that matches by range but
|
|
57
|
+
* fails either digest check reports `digest-mismatch` — it does NOT silently fall
|
|
58
|
+
* through to the ledger, because a corrupt exact shard is a fact worth surfacing.
|
|
59
|
+
*/
|
|
60
|
+
export function readExactShard(shards, range, digest) {
|
|
61
|
+
const byRange = shards.filter((s) => rangeEquals(s.range, range));
|
|
62
|
+
if (byRange.length === 0)
|
|
63
|
+
return { kind: "absent" };
|
|
64
|
+
const candidate = byRange.find((s) => s.digest === digest);
|
|
65
|
+
if (candidate === undefined)
|
|
66
|
+
return { kind: "digest-mismatch" };
|
|
67
|
+
// Defense in depth: the shard's recorded digest is metadata, the bytes are the
|
|
68
|
+
// authority. Re-hash what we actually hold.
|
|
69
|
+
const bytes = candidate.originalBytes;
|
|
70
|
+
if (sha256Hex(bytes) !== digest)
|
|
71
|
+
return { kind: "digest-mismatch" };
|
|
72
|
+
return { kind: "bytes", bytes };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Mode B — ledger range scan.
|
|
76
|
+
*
|
|
77
|
+
* An INDEPENDENT path from mode A: no shard index is consulted. The occurrence
|
|
78
|
+
* records covering `[seqStart..seqEnd]` are selected, sorted ascending by seq
|
|
79
|
+
* (the caller's array order is untrusted — a scan that concatenated records in
|
|
80
|
+
* arrival order would produce plausible-looking but wrong bytes), each record's
|
|
81
|
+
* own `bytesDigest` is verified, and the concatenation is hashed against the
|
|
82
|
+
* span digest.
|
|
83
|
+
*
|
|
84
|
+
* Both checks matter. Per-record verification localizes corruption to a single
|
|
85
|
+
* occurrence; the span-level hash catches a scan that is individually valid but
|
|
86
|
+
* collectively wrong — a missing record in the middle of the range, or a range
|
|
87
|
+
* that covers different content than the requester believed.
|
|
88
|
+
*/
|
|
89
|
+
export function readLedgerSpan(events, range, digest) {
|
|
90
|
+
const covering = events
|
|
91
|
+
.filter((e) => e.sessionId === range.sessionId &&
|
|
92
|
+
e.seq >= range.seqStart &&
|
|
93
|
+
e.seq <= range.seqEnd)
|
|
94
|
+
.sort((a, b) => (a.seq < b.seq ? -1 : a.seq > b.seq ? 1 : 0));
|
|
95
|
+
if (covering.length === 0)
|
|
96
|
+
return { kind: "absent" };
|
|
97
|
+
// Per-record byte authority (VC1A): a record whose own digest disagrees with
|
|
98
|
+
// its bytes is corrupt, and a corrupt record can never contribute to a restore.
|
|
99
|
+
for (const e of covering) {
|
|
100
|
+
if (sha256Hex(e.originalBytes) !== bareHex(e.bytesDigest)) {
|
|
101
|
+
return { kind: "digest-mismatch" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const total = covering.reduce((sum, e) => sum + e.originalBytes.length, 0);
|
|
105
|
+
const bytes = new Uint8Array(total);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
for (const e of covering) {
|
|
108
|
+
bytes.set(e.originalBytes, offset);
|
|
109
|
+
offset += e.originalBytes.length;
|
|
110
|
+
}
|
|
111
|
+
if (sha256Hex(bytes) !== digest)
|
|
112
|
+
return { kind: "digest-mismatch" };
|
|
113
|
+
return { kind: "bytes", bytes };
|
|
114
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
* Maximum spans in one request. A request naming more spans than this is
|
|
46
|
+
* rejected outright — the bound exists so a single call cannot be turned into an
|
|
47
|
+
* unbounded traversal of the ledger.
|
|
48
|
+
*/
|
|
49
|
+
export const RESTORE_LIMIT_SPANS = 64;
|
|
50
|
+
/**
|
|
51
|
+
* Maximum aggregate requested bytes (4 MiB), summed from the REQUEST's ranges
|
|
52
|
+
* (`byteEnd - byteStart`) so the bound is enforceable without reading anything.
|
|
53
|
+
*/
|
|
54
|
+
export const RESTORE_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
55
|
+
/**
|
|
56
|
+
* Registered VC6B conformance ID range (HEAL-016..030), continuing VC6A's
|
|
57
|
+
* HEAL-001..015. The acceptance test reads these rows from the v2 manifest and
|
|
58
|
+
* asserts each returns its manifest `ok`/`code`.
|
|
59
|
+
*/
|
|
60
|
+
export const RESTORE_IDS = Array.from({ length: 15 }, (_v, i) => `HEAL-${String(i + 16).padStart(3, "0")}`);
|
|
61
|
+
/** Named VC6B conformance assertions (the sprint's headline rows). */
|
|
62
|
+
export const RESTORE_NAMED_IDS = [
|
|
63
|
+
"HEAL-SPAN-001",
|
|
64
|
+
"HEAL-LIMIT-002",
|
|
65
|
+
"HEAL-DIGEST-003",
|
|
66
|
+
];
|