pi-mega-compact 0.20.4 → 0.20.6
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 +24 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +2 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-render.js +40 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-rollout.js +43 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +2 -0
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +5 -1
- package/dist/extensions/mega-context/vector-cortex.js +128 -0
- package/dist/extensions/mega-runtime/vector-cortex-live.js +105 -0
- package/dist/src/config/vector-cortex.js +24 -0
- package/dist/src/config.js +1 -1
- package/dist/src/store/sqlite/global-sessions.js +16 -4
- package/dist/src/vector-cortex/provider/registry.js +112 -0
- package/dist/src/vector-cortex/provider/types.js +30 -0
- package/dist/src/vector-cortex/render/_acceptance-fixture.js +58 -0
- package/dist/src/vector-cortex/render/_acceptance-helpers.js +17 -0
- package/dist/src/vector-cortex/render/_acceptance-provider.js +29 -0
- package/dist/src/vector-cortex/render/_acceptance-scenario.js +259 -0
- package/dist/src/vector-cortex/render/renderer.js +119 -0
- package/dist/src/vector-cortex/render/types.js +29 -0
- package/dist/src/vector-cortex/render/validator.js +90 -0
- package/dist/src/vector-cortex/rollout/_acceptance-fixture.js +55 -0
- package/dist/src/vector-cortex/rollout/_acceptance-helpers.js +8 -0
- package/dist/src/vector-cortex/rollout/_acceptance-scenario.js +70 -0
- package/dist/src/vector-cortex/rollout/assign.js +56 -0
- package/dist/src/vector-cortex/rollout/emit.js +50 -0
- package/dist/src/vector-cortex/rollout/gate.js +75 -0
- package/dist/src/vector-cortex/rollout/types.js +39 -0
- package/dist/vector-cortex/provider/registry.js +112 -0
- package/dist/vector-cortex/provider/types.js +30 -0
- package/dist/vector-cortex/render/_acceptance-fixture.js +58 -0
- package/dist/vector-cortex/render/_acceptance-helpers.js +17 -0
- package/dist/vector-cortex/render/_acceptance-provider.js +29 -0
- package/dist/vector-cortex/render/_acceptance-scenario.js +259 -0
- package/dist/vector-cortex/render/renderer.js +119 -0
- package/dist/vector-cortex/render/types.js +29 -0
- package/dist/vector-cortex/render/validator.js +90 -0
- package/dist/vector-cortex/rollout/_acceptance-fixture.js +55 -0
- package/dist/vector-cortex/rollout/_acceptance-helpers.js +8 -0
- package/dist/vector-cortex/rollout/_acceptance-scenario.js +70 -0
- package/dist/vector-cortex/rollout/assign.js +56 -0
- package/dist/vector-cortex/rollout/emit.js +50 -0
- package/dist/vector-cortex/rollout/gate.js +75 -0
- package/dist/vector-cortex/rollout/types.js +39 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-Cam9-nhT.js → AreaChart-DqTbaSRA.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-Cam9-nhT.js.map → AreaChart-DqTbaSRA.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-DzIY8aM1.js → BarChart-BfNBeOvF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-DzIY8aM1.js.map → BarChart-BfNBeOvF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CKIiYsG_.js → CacheTab-C9SOMYB5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CKIiYsG_.js.map → CacheTab-C9SOMYB5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-xaaBJLiK.js → EventsTab-GsMKP10_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-xaaBJLiK.js.map → EventsTab-GsMKP10_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-BGtUKOqG.js → HealthTab-DAOCXwRP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-BGtUKOqG.js.map → HealthTab-DAOCXwRP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ABgQnTZ9.js → MaintenanceTab-BtBeMkB5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ABgQnTZ9.js.map → MaintenanceTab-BtBeMkB5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-ChR2GAN-.js → MemoryMapTab-DzndtgkZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-ChR2GAN-.js.map → MemoryMapTab-DzndtgkZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DdCLAgcR.js → MetricsTab-DyxJ4eb8.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DdCLAgcR.js.map → MetricsTab-DyxJ4eb8.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DtemzJt6.js → OverviewTab-DCiEdVWR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DtemzJt6.js.map → OverviewTab-DCiEdVWR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-B9mvD9Ep.js → ReposTab-B5SIqVHA.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-B9mvD9Ep.js.map → ReposTab-B5SIqVHA.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcO38XLS.js → SessionsTab-DLWFJ9xI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcO38XLS.js.map → SessionsTab-DLWFJ9xI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-D_TBNP09.js → SetupTab-CvGXZyej.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-D_TBNP09.js.map → SetupTab-CvGXZyej.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-TjHpbUOo.js → TimeSavedCard-wZuQGxLS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-TjHpbUOo.js.map → TimeSavedCard-wZuQGxLS.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DprxndCF.js → TurnsTab-B7aOejch.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DprxndCF.js.map → TurnsTab-B7aOejch.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-OKgSo15G.js → WikiTab-DhWHjAtt.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-OKgSo15G.js.map → WikiTab-DhWHjAtt.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-D_Qz4q7L.js → button-BrC-W87h.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-D_Qz4q7L.js.map → button-BrC-W87h.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-CsI6Jm3p.js → card-0rgfRgr3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-CsI6Jm3p.js.map → card-0rgfRgr3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CvHKUdhX.js → generateCategoricalChart-DRresOQF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CvHKUdhX.js.map → generateCategoricalChart-DRresOQF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-0Ye_un22.js → index-DohmtZWL.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-0Ye_un22.js.map → index-DohmtZWL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-VQEoJl8Q.js → switch-Bs8g_zB5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-VQEoJl8Q.js.map → switch-Bs8g_zB5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-CAPm4HJo.js → toggle-DVVauIo8.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-CAPm4HJo.js.map → toggle-DVVauIo8.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-pxsNTVkh.js → useSSE-DxH1PTAR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-pxsNTVkh.js.map → useSSE-DxH1PTAR.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/VectorCortexMetric.tsx +10 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexPlansCard.tsx +39 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexRenderCard.tsx +38 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexRolloutCard.tsx +44 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +19 -38
- package/extensions/dashboard-client/src/types/vector-cortex.ts +26 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +52 -1
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +12 -0
- package/extensions/dashboard-server/routes-vector-cortex-render.ts +49 -0
- package/extensions/dashboard-server/routes-vector-cortex-rollout.ts +52 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +2 -0
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +4 -0
- package/extensions/mega-context/vector-cortex.ts +163 -0
- package/extensions/mega-runtime/vector-cortex-live.ts +165 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +26 -0
- package/src/config.ts +2 -0
- package/src/store/sqlite/global-sessions.ts +14 -2
- package/src/vector-cortex/provider/registry.ts +137 -0
- package/src/vector-cortex/provider/types.ts +163 -0
- package/src/vector-cortex/render/_acceptance-fixture.ts +128 -0
- package/src/vector-cortex/render/_acceptance-helpers.ts +39 -0
- package/src/vector-cortex/render/_acceptance-provider.ts +45 -0
- package/src/vector-cortex/render/_acceptance-scenario.ts +319 -0
- package/src/vector-cortex/render/renderer.ts +142 -0
- package/src/vector-cortex/render/types.ts +174 -0
- package/src/vector-cortex/render/validator.ts +120 -0
- package/src/vector-cortex/rollout/_acceptance-fixture.ts +94 -0
- package/src/vector-cortex/rollout/_acceptance-helpers.ts +24 -0
- package/src/vector-cortex/rollout/_acceptance-scenario.ts +96 -0
- package/src/vector-cortex/rollout/assign.ts +67 -0
- package/src/vector-cortex/rollout/emit.ts +81 -0
- package/src/vector-cortex/rollout/gate.ts +119 -0
- package/src/vector-cortex/rollout/types.ts +111 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Bym21yVQ.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Bym21yVQ.js.map +0 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/render/validator.ts — canonical request validator (VC5B, task 4).
|
|
3
|
+
*
|
|
4
|
+
* The validator runs BEFORE the provider invocation. It re-hashes the ENTIRE
|
|
5
|
+
* canonical outbound request and compares the result against the render manifest:
|
|
6
|
+
* - order — the request's node order equals the manifest's nodeOrder;
|
|
7
|
+
* - tools — every tool's bytes equal the source (exact-byte contract,
|
|
8
|
+
* PREVENT-PI-002);
|
|
9
|
+
* - byte lengths — every segment length equals the pinned length;
|
|
10
|
+
* - provider — the active profile still matches the manifest profile, and
|
|
11
|
+
* no provider constraint is violated.
|
|
12
|
+
*
|
|
13
|
+
* UNIQUE failure injection: the provider profile is changed AFTER render but
|
|
14
|
+
* BEFORE validation. The re-hashed request (folded under the new profile's
|
|
15
|
+
* hashMode/stableFields) no longer matches the manifest digest → the validator
|
|
16
|
+
* returns `REN_PROFILE_DIGEST_MISMATCH` and the host selects triad C (predecessor
|
|
17
|
+
* prompt path). A mismatch under an unchanged profile is an exact-byte/order
|
|
18
|
+
* violation (REN_ORDER_MISMATCH / REN_TOOL_BYTE_MISMATCH / REN_BYTE_LENGTH_MISMATCH
|
|
19
|
+
* / REN_PROVIDER_CONSTRAINT_VIOLATED).
|
|
20
|
+
*
|
|
21
|
+
* Pi-agnostic, dependency-free, zero network (PREVENT-PI-004 / PREVENT-011).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
CanonicalOutboundRequest,
|
|
28
|
+
RenderFailureCode,
|
|
29
|
+
RenderManifestV1,
|
|
30
|
+
RenderMode,
|
|
31
|
+
} from "./types.js";
|
|
32
|
+
import { canonicalRequestBytes } from "./renderer.js";
|
|
33
|
+
import type { ProviderProfileBundle } from "../provider/types.js";
|
|
34
|
+
|
|
35
|
+
/** A validated request verdict. `ok:true` carries the recomputed digest. */
|
|
36
|
+
export type ValidateResult =
|
|
37
|
+
| { readonly ok: true; readonly requestDigest: string }
|
|
38
|
+
| {
|
|
39
|
+
readonly ok: false;
|
|
40
|
+
readonly code: RenderFailureCode;
|
|
41
|
+
/** The triad the host must select. C = predecessor prompt path. */
|
|
42
|
+
readonly triad: "A" | "C";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function digestOf(req: CanonicalOutboundRequest): string {
|
|
46
|
+
return createHash("sha256").update(canonicalRequestBytes(req)).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate the canonical outbound request against the render manifest, using the
|
|
51
|
+
* profile that is active AT VALIDATION TIME. `sourceToolBytes` pins the exact
|
|
52
|
+
* source tool bytes keyed by node ID so the validator can detect a byte change.
|
|
53
|
+
*/
|
|
54
|
+
export function validateRender(
|
|
55
|
+
manifest: RenderManifestV1,
|
|
56
|
+
request: CanonicalOutboundRequest,
|
|
57
|
+
profile: ProviderProfileBundle | null,
|
|
58
|
+
sourceToolBytes: ReadonlyMap<string, string>,
|
|
59
|
+
pinnedToolLengths: ReadonlyMap<string, number>,
|
|
60
|
+
): ValidateResult {
|
|
61
|
+
// 1. Order must equal the manifest's nodeOrder exactly (validator order replayed).
|
|
62
|
+
if (request.nodes.length !== manifest.nodeOrder.length) {
|
|
63
|
+
return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < manifest.nodeOrder.length; i++) {
|
|
66
|
+
if (request.nodes[i].id !== manifest.nodeOrder[i]) {
|
|
67
|
+
return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Tool byte LENGTHS must match the pinned source lengths FIRST (catch a
|
|
72
|
+
// silent truncation/transcode as the more specific signal), then the exact
|
|
73
|
+
// byte content (PREVENT-PI-002). A length change is reported as
|
|
74
|
+
// REN_BYTE_LENGTH_MISMATCH before falling through to the content check.
|
|
75
|
+
if (request.tools.length !== sourceToolBytes.size) {
|
|
76
|
+
return { ok: false, code: "REN_TOOL_BYTE_MISMATCH", triad: "A" };
|
|
77
|
+
}
|
|
78
|
+
for (const t of request.tools) {
|
|
79
|
+
const src = sourceToolBytes.get(t.id);
|
|
80
|
+
if (src === undefined) {
|
|
81
|
+
return { ok: false, code: "REN_TOOL_BYTE_MISMATCH", triad: "A" };
|
|
82
|
+
}
|
|
83
|
+
const pinned = pinnedToolLengths.get(t.id);
|
|
84
|
+
if (pinned !== undefined && Buffer.byteLength(t.bytes, "utf8") !== pinned) {
|
|
85
|
+
return { ok: false, code: "REN_BYTE_LENGTH_MISMATCH", triad: "A" };
|
|
86
|
+
}
|
|
87
|
+
if (src !== t.bytes) {
|
|
88
|
+
return { ok: false, code: "REN_TOOL_BYTE_MISMATCH", triad: "A" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Provider constraint: the active profile at validation time must match the
|
|
93
|
+
// manifest's profile. A changed profile is the UNIQUE injection → mismatch.
|
|
94
|
+
if (profile === null) {
|
|
95
|
+
// Clean bypass case: the manifest should also be "unknown"; if it isn't, the
|
|
96
|
+
// profile was swapped after render → mismatch.
|
|
97
|
+
if (manifest.profileId !== "unknown") {
|
|
98
|
+
return { ok: false, code: "REN_PROFILE_DIGEST_MISMATCH", triad: "C" };
|
|
99
|
+
}
|
|
100
|
+
} else if (profile.profile.id !== manifest.profileId || profile.profile.version !== manifest.profileVersion) {
|
|
101
|
+
return { ok: false, code: "REN_PROFILE_DIGEST_MISMATCH", triad: "C" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Re-hash the ENTIRE canonical outbound request under the active profile's
|
|
105
|
+
// hash mode (entire-canonical-request is the only supported mode) and compare.
|
|
106
|
+
const digest = digestOf(request);
|
|
107
|
+
if (digest !== manifest.requestDigest) {
|
|
108
|
+
// A digest change under a consistent profile is a byte/order violation, not a
|
|
109
|
+
// profile swap. (The profile-swap case above already returned C.)
|
|
110
|
+
return { ok: false, code: "REN_PROVIDER_CONSTRAINT_VIOLATED", triad: "A" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { ok: true, requestDigest: digest };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Triad selection helper: fold a validation outcome into the host's triad choice. */
|
|
117
|
+
export function selectTriad(result: ValidateResult): RenderMode {
|
|
118
|
+
if (result.ok) return "A";
|
|
119
|
+
return result.triad; // "C" for profile swap / unknown; stays "A" otherwise
|
|
120
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rollout/_acceptance-fixture.ts — conformance fixture I/O for VC5C rollout
|
|
3
|
+
* acceptance rows.
|
|
4
|
+
*
|
|
5
|
+
* Reads the v2 conformance manifest + the per-sprint rollout fixture files, and
|
|
6
|
+
* exposes the `RolloutFx` shape the acceptance aggregator drives. No
|
|
7
|
+
* mocks/stubs: the fixtures are the committed canonical corpus.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
function repoRoot(from: string): string {
|
|
17
|
+
let dir = from;
|
|
18
|
+
for (let i = 0; i < 8; i++) {
|
|
19
|
+
if (existsSync(join(dir, "conformance", "vector-cortex"))) return dir;
|
|
20
|
+
const next = dirname(dir);
|
|
21
|
+
if (next === dir) break;
|
|
22
|
+
dir = next;
|
|
23
|
+
}
|
|
24
|
+
throw new Error("conformance corpus not found above " + from);
|
|
25
|
+
}
|
|
26
|
+
export const REPO_ROOT = repoRoot(HERE);
|
|
27
|
+
export const V2 = join(REPO_ROOT, "conformance", "vector-cortex", "v2");
|
|
28
|
+
|
|
29
|
+
export interface ManifestRow {
|
|
30
|
+
id: string;
|
|
31
|
+
path: string;
|
|
32
|
+
algorithm: string;
|
|
33
|
+
expected: string;
|
|
34
|
+
}
|
|
35
|
+
export interface Manifest {
|
|
36
|
+
fixtures: ManifestRow[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readManifest(): Manifest {
|
|
40
|
+
return JSON.parse(readFileSync(join(V2, "manifest.json"), "utf8")) as Manifest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readFixture(id: string, prefix: "rollout"): unknown {
|
|
44
|
+
const m = readManifest();
|
|
45
|
+
const row = m.fixtures.find((f) => f.id === id && f.path.startsWith(`${prefix}/`));
|
|
46
|
+
assert.ok(row, `fixture ${id} registered under ${prefix}/ in manifest`);
|
|
47
|
+
return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RolloutFxEvidence {
|
|
51
|
+
windowStartMs?: number;
|
|
52
|
+
powered?: boolean;
|
|
53
|
+
events?: number;
|
|
54
|
+
sessions?: number;
|
|
55
|
+
hardFaults?: Array<{ kind: string; detail: string }>;
|
|
56
|
+
}
|
|
57
|
+
export interface RolloutFxInput {
|
|
58
|
+
scenario: string;
|
|
59
|
+
sessionId?: string;
|
|
60
|
+
evidence?: RolloutFxEvidence;
|
|
61
|
+
}
|
|
62
|
+
export interface RolloutFxExpected {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
bucket?: number;
|
|
65
|
+
gateIndex?: number;
|
|
66
|
+
promotionBlocked?: boolean;
|
|
67
|
+
selectsPreVc?: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface RolloutFx {
|
|
70
|
+
id: string;
|
|
71
|
+
schema: string;
|
|
72
|
+
producer: string;
|
|
73
|
+
assertion: string;
|
|
74
|
+
kind: string;
|
|
75
|
+
input: RolloutFxInput;
|
|
76
|
+
expected: RolloutFxExpected;
|
|
77
|
+
}
|
|
78
|
+
export function rolloutFixture(id: string): RolloutFx {
|
|
79
|
+
return readFixture(id, "rollout") as RolloutFx;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Flag-pinned wrapper: VC5C gated by MEGACOMPACT_VC5C (defaults ON). */
|
|
83
|
+
export function withFlagsOn(fn: () => void): () => void {
|
|
84
|
+
return (): void => {
|
|
85
|
+
const saved = process.env.MEGACOMPACT_VC5C;
|
|
86
|
+
process.env.MEGACOMPACT_VC5C = "1";
|
|
87
|
+
try {
|
|
88
|
+
fn();
|
|
89
|
+
} finally {
|
|
90
|
+
if (saved === undefined) delete process.env.MEGACOMPACT_VC5C;
|
|
91
|
+
else process.env.MEGACOMPACT_VC5C = saved;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rollout/_acceptance-helpers.ts — pure barrel re-export of the VC5C rollout
|
|
3
|
+
* acceptance helpers (fixture I/O + REAL runner). Mirrors the render family's
|
|
4
|
+
* delegate-shell split so the aggregator stays under the 600-line test hard
|
|
5
|
+
* limit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
REPO_ROOT,
|
|
10
|
+
V2,
|
|
11
|
+
readManifest,
|
|
12
|
+
rolloutFixture,
|
|
13
|
+
withFlagsOn,
|
|
14
|
+
} from "./_acceptance-fixture.js";
|
|
15
|
+
export type {
|
|
16
|
+
Manifest,
|
|
17
|
+
ManifestRow,
|
|
18
|
+
RolloutFx,
|
|
19
|
+
RolloutFxInput,
|
|
20
|
+
RolloutFxExpected,
|
|
21
|
+
RolloutFxEvidence,
|
|
22
|
+
} from "./_acceptance-fixture.js";
|
|
23
|
+
export { runRolloutScenario } from "./_acceptance-scenario.js";
|
|
24
|
+
export type { RolloutRunOutcome } from "./_acceptance-scenario.js";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rollout/_acceptance-scenario.ts — the REAL rollout runner for VC5C acceptance
|
|
3
|
+
* rows. Drives `assignSession` + `decideGate` (no mocks) against the fixture
|
|
4
|
+
* `input` and returns the result fields the fixture's `expected` block pins.
|
|
5
|
+
*
|
|
6
|
+
* Fake clocks: the gate scenario accepts an injected monotonic clock so the
|
|
7
|
+
* spec's unique failure injection (restart at 71h59m with a wall-clock jump +1d
|
|
8
|
+
* but monotonic unchanged) is reproducible. The wall-clock jump is irrelevant to
|
|
9
|
+
* the safety decision because eligibility uses the MONOTONIC delta only.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { assignSession } from "./assign.js";
|
|
13
|
+
import {
|
|
14
|
+
decideGate,
|
|
15
|
+
type RolloutClock,
|
|
16
|
+
type RolloutEvidence,
|
|
17
|
+
} from "./gate.js";
|
|
18
|
+
import type { GateIndex, RolloutHardFault } from "./types.js";
|
|
19
|
+
import type { RolloutFx } from "./_acceptance-fixture.js";
|
|
20
|
+
|
|
21
|
+
const GATE_MIN_ELAPSED_MS = 72 * 60 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
export interface RolloutRunOutcome {
|
|
24
|
+
readonly ok: boolean;
|
|
25
|
+
readonly bucket?: number;
|
|
26
|
+
readonly gateIndex?: number;
|
|
27
|
+
readonly promotionBlocked?: boolean;
|
|
28
|
+
readonly selectsPreVc?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Fixed monotonic clock: elapsed = now - windowStart (the only driver). */
|
|
32
|
+
function clockAt(elapsedMs: number, windowStartMs: number): RolloutClock {
|
|
33
|
+
return {
|
|
34
|
+
now: () => windowStartMs + elapsedMs, // monotonic delta is the truth
|
|
35
|
+
wallNow: () => new Date(1_700_000_000_000 + elapsedMs).toISOString(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Run a rollout fixture through the REAL assign + gate pipeline. */
|
|
40
|
+
export function runRolloutScenario(fx: RolloutFx): RolloutRunOutcome {
|
|
41
|
+
const scenario = fx.input.scenario;
|
|
42
|
+
|
|
43
|
+
if (scenario === "assign-stable") {
|
|
44
|
+
const a = assignSession(fx.input.sessionId ?? "");
|
|
45
|
+
const b = assignSession(fx.input.sessionId ?? "");
|
|
46
|
+
// Determinism: recomputing yields the identical bucket.
|
|
47
|
+
if (a.bucket !== b.bucket) {
|
|
48
|
+
return { ok: false };
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, bucket: a.bucket };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (scenario === "gate-power" || scenario === "gate-safety") {
|
|
54
|
+
const ev = fx.input.evidence ?? {};
|
|
55
|
+
const windowStartMs = ev.windowStartMs ?? 0;
|
|
56
|
+
const hardFaults = (ev.hardFaults ?? []).map(
|
|
57
|
+
(f): RolloutHardFault => ({ kind: f.kind as RolloutHardFault["kind"], detail: f.detail }),
|
|
58
|
+
);
|
|
59
|
+
const evidence: RolloutEvidence = {
|
|
60
|
+
windowStartMs,
|
|
61
|
+
powered: ev.powered ?? false,
|
|
62
|
+
events: ev.events ?? 0,
|
|
63
|
+
sessions: ev.sessions ?? 0,
|
|
64
|
+
hardFaults,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// The spec's unique failure injection: wall-clock jump is irrelevant; only the
|
|
68
|
+
// monotonic elapsed drives eligibility. For the 71h59m row we pass 71h59m; for
|
|
69
|
+
// the wall-jump row we pass the SAME monotonic elapsed (so it must stay blocked);
|
|
70
|
+
// for the >=72h row we pass a full 72h+ monotonic delta (so it advances).
|
|
71
|
+
const start = (fx as RolloutFx & { _monotonicElapsedMs?: number })._monotonicElapsedMs;
|
|
72
|
+
const elapsedMs = start ?? GATE_MIN_ELAPSED_MS; // default: a full 72h window
|
|
73
|
+
const clock = clockAt(elapsedMs, windowStartMs);
|
|
74
|
+
|
|
75
|
+
// gate-safety with a hard fault forces pre-VC + freezes promotion regardless of
|
|
76
|
+
// the monotonic window; the gate index stays at the input's prior gate (0 here).
|
|
77
|
+
if (scenario === "gate-safety") {
|
|
78
|
+
const outcome = decideGate(0 as GateIndex, evidence, clock);
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
gateIndex: outcome.gateIndex,
|
|
82
|
+
promotionBlocked: outcome.promotionBlocked,
|
|
83
|
+
selectsPreVc: outcome.promotionBlocked,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const outcome = decideGate(0 as GateIndex, evidence, clock);
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
gateIndex: outcome.gateIndex,
|
|
91
|
+
promotionBlocked: outcome.promotionBlocked,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error(`unknown rollout scenario: ${scenario}`);
|
|
96
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/rollout/assign.ts — deterministic stable-bucket assignment.
|
|
3
|
+
*
|
|
4
|
+
* `assignSession(sessionId)` hashes a session id into one of 10,000 stable
|
|
5
|
+
* buckets (0..9999). The assignment is a PURE function of the session id and
|
|
6
|
+
* NEVER changes across process restart: it uses a fixed-seed FNV-1a over the
|
|
7
|
+
* session id bytes, never Date.now / Math.random (PREVENT-PI-004-adjacent
|
|
8
|
+
* determinism contract; the spec's restart-invariance requirement).
|
|
9
|
+
*
|
|
10
|
+
* A bucket `b` is "in" gate `g` (one of [1,5,25,50,100]) iff
|
|
11
|
+
* `b < g * 100`. So gate 1% covers buckets 0..99, gate 100% covers 0..9999.
|
|
12
|
+
*
|
|
13
|
+
* Pure: no I/O, no clock, no network. PREVENT-011: no `any`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
ROLLOUT_BUCKETS,
|
|
18
|
+
ROLLOUT_GATES,
|
|
19
|
+
type GateIndex,
|
|
20
|
+
type RolloutAssignmentV1,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Deterministic 32-bit FNV-1a over the session id (UTF-8 bytes). Fixed seed
|
|
25
|
+
* (2166136261) — restart-invariant by construction. Returns a bucket in
|
|
26
|
+
* 0..(ROLLOUT_BUCKETS-1).
|
|
27
|
+
*/
|
|
28
|
+
export function assignSession(sessionId: string): RolloutAssignmentV1 {
|
|
29
|
+
const enc = new TextEncoder();
|
|
30
|
+
const bytes = enc.encode(sessionId);
|
|
31
|
+
let hash = 0x811c9dc5; // FNV offset basis (32-bit)
|
|
32
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
33
|
+
hash ^= bytes[i]!;
|
|
34
|
+
// hash *= 16777619 (FNV prime) with 32-bit overflow wrap.
|
|
35
|
+
hash = Math.imul(hash, 0x01000193);
|
|
36
|
+
}
|
|
37
|
+
const bucket = (hash >>> 0) % ROLLOUT_BUCKETS;
|
|
38
|
+
|
|
39
|
+
// The bucket's qualifying gate is the HIGHEST gate whose bound it falls under.
|
|
40
|
+
let gateIndex: GateIndex = 0;
|
|
41
|
+
for (let i = 0; i < ROLLOUT_GATES.length; i++) {
|
|
42
|
+
if (bucket < ROLLOUT_GATES[i]! * 100) {
|
|
43
|
+
gateIndex = i as GateIndex;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
schema: "rollout-assignment-v1",
|
|
50
|
+
sessionId,
|
|
51
|
+
bucket,
|
|
52
|
+
gateIndex,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether a bucket qualifies for a given gate percentage. Used by the live seam
|
|
58
|
+
* to decide whether a session's bucket is currently exposed to the VC path.
|
|
59
|
+
*/
|
|
60
|
+
export function bucketInGate(bucket: number, gatePct: number): boolean {
|
|
61
|
+
return bucket < gatePct * 100;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Current gate percentage for a gate index. */
|
|
65
|
+
export function gatePctForIndex(gateIndex: GateIndex): number {
|
|
66
|
+
return ROLLOUT_GATES[gateIndex]!;
|
|
67
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/rollout/emit.ts — VC5C rollout emit seam.
|
|
3
|
+
*
|
|
4
|
+
* The structured-event surface for the live graduated rollout: assignment and
|
|
5
|
+
* promotion-blocked observability. Mirrors the VC0C resilience emit seam and
|
|
6
|
+
* the VC0B replay emit seam: the same injected `(event, fields)` callback, the
|
|
7
|
+
* same non-fatal `safe()` best-effort wrapper, and the same per-call
|
|
8
|
+
* `VC5C_ENABLED()` gate. Absent emitter (or flag OFF) degrades to a no-op with
|
|
9
|
+
* byte-identical predecessor behavior (PREVENT-PI-004, structured `ts`+`event`).
|
|
10
|
+
*
|
|
11
|
+
* Carries exactly the two events the sprint names:
|
|
12
|
+
* - vector_cortex_rollout_assigned (a session was assigned a bucket)
|
|
13
|
+
* - vector_cortex_rollout_promotion_blocked (a hard fault froze promotion)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { VC5C_ENABLED } from "../../config/vector-cortex.js";
|
|
17
|
+
|
|
18
|
+
export type RolloutEventName =
|
|
19
|
+
| "vector_cortex_rollout_assigned"
|
|
20
|
+
| "vector_cortex_rollout_promotion_blocked";
|
|
21
|
+
|
|
22
|
+
/** Injected emit callback — same (event, fields) shape as the other seams. */
|
|
23
|
+
export type RolloutEmitter = (event: string, fields: Record<string, unknown>) => void;
|
|
24
|
+
|
|
25
|
+
export interface RolloutAssignedFields {
|
|
26
|
+
readonly sessionId: string;
|
|
27
|
+
readonly bucket: number;
|
|
28
|
+
readonly gateIndex: number;
|
|
29
|
+
readonly gatePct: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PromotionBlockedFields {
|
|
33
|
+
readonly gateIndex: number;
|
|
34
|
+
readonly kind: string;
|
|
35
|
+
readonly detail: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Typed, best-effort reporter bound to the two rollout event names. */
|
|
39
|
+
export interface RolloutReporter {
|
|
40
|
+
readonly assigned: (fields: RolloutAssignedFields) => void;
|
|
41
|
+
readonly promotionBlocked: (fields: PromotionBlockedFields) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safe(fn: (event: RolloutEventName, fields: Record<string, unknown>) => void): (
|
|
45
|
+
event: RolloutEventName,
|
|
46
|
+
fields: Record<string, unknown>,
|
|
47
|
+
) => void {
|
|
48
|
+
return (event, fields) => {
|
|
49
|
+
try {
|
|
50
|
+
fn(event, fields);
|
|
51
|
+
} catch {
|
|
52
|
+
/* non-fatal observability — never break the agent loop */
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build a typed reporter over an injected emit callback. Emission is gated on
|
|
59
|
+
* `VC5C_ENABLED()` — flag OFF (or absent emitter) means zero rollout
|
|
60
|
+
* observability writes (pre-VC predecessor parity). The flag is read per-call so
|
|
61
|
+
* a live toggle takes effect immediately.
|
|
62
|
+
*/
|
|
63
|
+
export function createRolloutReporter(emit?: RolloutEmitter): RolloutReporter {
|
|
64
|
+
const fire = safe((event, fields) => {
|
|
65
|
+
if (VC5C_ENABLED()) emit?.(event, fields);
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
assigned(fields) {
|
|
69
|
+
fire("vector_cortex_rollout_assigned", fields as unknown as Record<string, unknown>);
|
|
70
|
+
},
|
|
71
|
+
promotionBlocked(fields) {
|
|
72
|
+
fire("vector_cortex_rollout_promotion_blocked", fields as unknown as Record<string, unknown>);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Export the event names for consumers that match on them. */
|
|
78
|
+
export const ROLLOUT_EVENT_NAMES: readonly RolloutEventName[] = [
|
|
79
|
+
"vector_cortex_rollout_assigned",
|
|
80
|
+
"vector_cortex_rollout_promotion_blocked",
|
|
81
|
+
];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/rollout/gate.ts — monotonic graduated-gate advance decision.
|
|
3
|
+
*
|
|
4
|
+
* A gate advances exactly ONE step only when ALL conjuncts hold:
|
|
5
|
+
* (elapsedWallClock >= 72h) — measured on the MONOTONIC clock, not wall time
|
|
6
|
+
* AND (powered sample available)
|
|
7
|
+
* AND (events >= 10000)
|
|
8
|
+
* AND (sessions >= 200)
|
|
9
|
+
*
|
|
10
|
+
* Promotion is strictly monotonic by one gate step. The unique failure
|
|
11
|
+
* injection (TRIAD_RESILIENCE.md): a wall-clock jump while the monotonic clock
|
|
12
|
+
* is unchanged does NOT advance — because the 72h eligibility is the MONOTONIC
|
|
13
|
+
* delta, not wall time. We therefore take an injected clock seam with SEPARATE
|
|
14
|
+
* monotonic and wall readings and never call Date.now() directly for the safety
|
|
15
|
+
* decision (PREVENT decorum: the host injects a testable fake clock).
|
|
16
|
+
*
|
|
17
|
+
* Pure arithmetic over injected evidence + clock; no I/O, no network.
|
|
18
|
+
* PREVENT-011: no `any`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
ROLLOUT_GATES,
|
|
23
|
+
type GateIndex,
|
|
24
|
+
type LiveOutcomeV1,
|
|
25
|
+
type RolloutHardFault,
|
|
26
|
+
} from "./types.js";
|
|
27
|
+
|
|
28
|
+
/** The 72h monotonic residency requirement (milliseconds). */
|
|
29
|
+
export const GATE_MIN_ELAPSED_MS = 72 * 60 * 60 * 1000;
|
|
30
|
+
/** Powered-sample availability gate (minimum events for a one-sided CI read). */
|
|
31
|
+
export const GATE_MIN_EVENTS = 10_000;
|
|
32
|
+
/** Minimum distinct sessions before a gate may advance. */
|
|
33
|
+
export const GATE_MIN_SESSIONS = 200;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Injected clock seam. `now()` is MONOTONIC time (fake-clock testable; never
|
|
37
|
+
* wall time) — eligibility windows/cooldowns use it. `wallNow()` is wall time
|
|
38
|
+
* used ONLY for records (the `decidedAt` field), never for eligibility.
|
|
39
|
+
*/
|
|
40
|
+
export interface RolloutClock {
|
|
41
|
+
/** Monotonic milliseconds (e.g. process monotonic time, fake-clock testable). */
|
|
42
|
+
now(): number;
|
|
43
|
+
/** Wall-clock ISO-8601 string for records only. */
|
|
44
|
+
wallNow(): string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Default clock: real monotonic (performance.now) + wall (ISO). */
|
|
48
|
+
export function defaultClock(): RolloutClock {
|
|
49
|
+
return {
|
|
50
|
+
now: () => performance.now(),
|
|
51
|
+
wallNow: () => new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Evidence observed in the current observation window. */
|
|
56
|
+
export interface RolloutEvidence {
|
|
57
|
+
/** Monotonic start of the observation window (ms). */
|
|
58
|
+
readonly windowStartMs: number;
|
|
59
|
+
/** Whether a powered (statistically adequate) sample is available. */
|
|
60
|
+
readonly powered: boolean;
|
|
61
|
+
/** Total observed events in the window. */
|
|
62
|
+
readonly events: number;
|
|
63
|
+
/** Total distinct sessions observed in the window. */
|
|
64
|
+
readonly sessions: number;
|
|
65
|
+
/** Hard failures observed (any entry freezes promotion → select C). */
|
|
66
|
+
readonly hardFaults: readonly RolloutHardFault[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decide the next live outcome for a decision epoch. Returns the (possibly
|
|
71
|
+
* advanced) gate index + the full evidence snapshot. Promotion monotonic by one
|
|
72
|
+
* step; a hard fault freezes promotion (promotionBlocked=true) and is reported
|
|
73
|
+
* for C-selection by the live seam.
|
|
74
|
+
*
|
|
75
|
+
* @param currentGate the gate index BEFORE this decision (0..4).
|
|
76
|
+
* @param evidence the observed window evidence.
|
|
77
|
+
* @param clock the injected monotonic/wall clock seam.
|
|
78
|
+
*/
|
|
79
|
+
export function decideGate(
|
|
80
|
+
currentGate: GateIndex,
|
|
81
|
+
evidence: RolloutEvidence,
|
|
82
|
+
clock: RolloutClock,
|
|
83
|
+
): LiveOutcomeV1 {
|
|
84
|
+
const elapsedMs = Math.max(0, clock.now() - evidence.windowStartMs);
|
|
85
|
+
|
|
86
|
+
const hardFault = evidence.hardFaults.length > 0;
|
|
87
|
+
const eligible =
|
|
88
|
+
!hardFault &&
|
|
89
|
+
currentGate < (ROLLOUT_GATES.length - 1) &&
|
|
90
|
+
elapsedMs >= GATE_MIN_ELAPSED_MS &&
|
|
91
|
+
evidence.powered &&
|
|
92
|
+
evidence.events >= GATE_MIN_EVENTS &&
|
|
93
|
+
evidence.sessions >= GATE_MIN_SESSIONS;
|
|
94
|
+
|
|
95
|
+
// Monotonic by ONE step only.
|
|
96
|
+
const nextGate: GateIndex = eligible
|
|
97
|
+
? ((currentGate + 1) as GateIndex)
|
|
98
|
+
: currentGate;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
schema: "live-outcome-v1",
|
|
102
|
+
gateIndex: nextGate,
|
|
103
|
+
gatePct: ROLLOUT_GATES[nextGate]!,
|
|
104
|
+
elapsedMs,
|
|
105
|
+
powered: evidence.powered,
|
|
106
|
+
events: evidence.events,
|
|
107
|
+
sessions: evidence.sessions,
|
|
108
|
+
promotionBlocked: hardFault,
|
|
109
|
+
decidedAt: clock.wallNow(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether a hard fault should SELECT triad C (pre-VC path). Any hard causal/
|
|
115
|
+
* tool/anchor/exact failure does. Exposed so the live seam can branch cleanly.
|
|
116
|
+
*/
|
|
117
|
+
export function selectsPreVcPath(fault: RolloutHardFault | undefined): boolean {
|
|
118
|
+
return fault !== undefined;
|
|
119
|
+
}
|