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,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render/_acceptance-fixture.ts — conformance fixture I/O for VC5B render +
|
|
3
|
+
* provider acceptance rows.
|
|
4
|
+
*
|
|
5
|
+
* Reads the v2 conformance manifest + the per-sprint fixture files, and exposes
|
|
6
|
+
* the `RenderFx` / `ProviderFx` shapes 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
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
function repoRoot(from) {
|
|
15
|
+
let dir = from;
|
|
16
|
+
for (let i = 0; i < 8; i++) {
|
|
17
|
+
if (existsSync(join(dir, "conformance", "vector-cortex")))
|
|
18
|
+
return dir;
|
|
19
|
+
const next = dirname(dir);
|
|
20
|
+
if (next === dir)
|
|
21
|
+
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
|
+
export function readManifest() {
|
|
29
|
+
return JSON.parse(readFileSync(join(V2, "manifest.json"), "utf8"));
|
|
30
|
+
}
|
|
31
|
+
function readFixture(id, prefix) {
|
|
32
|
+
const m = readManifest();
|
|
33
|
+
const row = m.fixtures.find((f) => f.id === id && f.path.startsWith(`${prefix}/`));
|
|
34
|
+
assert.ok(row, `fixture ${id} registered under ${prefix}/ in manifest`);
|
|
35
|
+
return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
|
|
36
|
+
}
|
|
37
|
+
export function renderFixture(id) {
|
|
38
|
+
return readFixture(id, "render");
|
|
39
|
+
}
|
|
40
|
+
export function providerFixture(id) {
|
|
41
|
+
return readFixture(id, "provider");
|
|
42
|
+
}
|
|
43
|
+
/** Flag-pinned wrapper: VC5B gated by MEGACOMPACT_VC5B (defaults ON). */
|
|
44
|
+
export function withFlagsOn(fn) {
|
|
45
|
+
return () => {
|
|
46
|
+
const saved = process.env.MEGACOMPACT_VC5B;
|
|
47
|
+
process.env.MEGACOMPACT_VC5B = "1";
|
|
48
|
+
try {
|
|
49
|
+
fn();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (saved === undefined)
|
|
53
|
+
delete process.env.MEGACOMPACT_VC5B;
|
|
54
|
+
else
|
|
55
|
+
process.env.MEGACOMPACT_VC5B = saved;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render/_acceptance-helpers.ts — barrel re-export for the VC5B render + provider
|
|
3
|
+
* acceptance rows.
|
|
4
|
+
*
|
|
5
|
+
* The implementation is split across focused sibling files to keep each under
|
|
6
|
+
* the src/ 500-line hard limit; this file is the single import surface the
|
|
7
|
+
* acceptance aggregator uses:
|
|
8
|
+
* - `_acceptance-fixture.ts` — manifest I/O + RenderFx/ProviderFx shapes + withFlagsOn
|
|
9
|
+
* - `_acceptance-scenario.ts` — declarative graph materialization + runRenderScenario
|
|
10
|
+
* - `_acceptance-provider.ts` — provider-registry runner + KNOWN_KEYS
|
|
11
|
+
*
|
|
12
|
+
* Every function drives the REAL render / validate / provider logic — no mocks,
|
|
13
|
+
* no stubs.
|
|
14
|
+
*/
|
|
15
|
+
export { REPO_ROOT, V2, readManifest, renderFixture, providerFixture, withFlagsOn, } from "./_acceptance-fixture.js";
|
|
16
|
+
export { materializeGraph, runRenderScenario } from "./_acceptance-scenario.js";
|
|
17
|
+
export { runProviderScenario, KNOWN_KEYS } from "./_acceptance-provider.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render/_acceptance-provider.ts — the REAL provider-registry runner for VC5B
|
|
3
|
+
* provider acceptance rows. Drives `resolveProviderProfile` (no mocks).
|
|
4
|
+
*/
|
|
5
|
+
import { resolveProviderProfile, KNOWN_PROVIDER_KEYS } from "../provider/registry.js";
|
|
6
|
+
/** Run a provider fixture through the REAL registry resolver. */
|
|
7
|
+
export function runProviderScenario(fx) {
|
|
8
|
+
const r = resolveProviderProfile(fx.input.provider, fx.input.model);
|
|
9
|
+
if (!r.ok) {
|
|
10
|
+
return { ok: false, code: r.code, bypassClean: true };
|
|
11
|
+
}
|
|
12
|
+
const p = r.bundle.profile;
|
|
13
|
+
const cacheStable = fx.input.scenario === "resolve-excluded-cache" &&
|
|
14
|
+
p.excludedJsonPointers.length === 1 &&
|
|
15
|
+
p.excludedJsonPointers[0].pointer === "/requestId";
|
|
16
|
+
// deterministic: a second identical lookup returns the same profile id.
|
|
17
|
+
const r2 = resolveProviderProfile(fx.input.provider, fx.input.model);
|
|
18
|
+
const deterministic = r2.ok && r2.bundle.profile.id === p.id;
|
|
19
|
+
return {
|
|
20
|
+
ok: true,
|
|
21
|
+
profileId: p.id,
|
|
22
|
+
hashMode: p.hashMode,
|
|
23
|
+
excludedPointers: p.excludedJsonPointers.map((e) => e.pointer),
|
|
24
|
+
cacheStable,
|
|
25
|
+
deterministic,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Exposed for the acceptance registration check. */
|
|
29
|
+
export const KNOWN_KEYS = KNOWN_PROVIDER_KEYS;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render/_acceptance-scenario.ts — declarative graph materialization + the REAL
|
|
3
|
+
* render/validate runner for VC5B acceptance rows.
|
|
4
|
+
*
|
|
5
|
+
* `materializeGraph(name)` builds a node map + stable order for a named graph;
|
|
6
|
+
* `runRenderScenario(fx)` drives the REAL renderPrompt + validateRender against
|
|
7
|
+
* the resolved provider profile and returns the result fields the fixture's
|
|
8
|
+
* `expected` block pins. No mocks, no stubs — every assertion runs against the
|
|
9
|
+
* production logic.
|
|
10
|
+
*/
|
|
11
|
+
import { renderPrompt } from "./renderer.js";
|
|
12
|
+
import { validateRender } from "./validator.js";
|
|
13
|
+
import { resolveProviderProfile } from "../provider/registry.js";
|
|
14
|
+
const enc = new TextEncoder();
|
|
15
|
+
function payload(id, label) {
|
|
16
|
+
return enc.encode(`${id}:${label}`);
|
|
17
|
+
}
|
|
18
|
+
/** Build a deterministic graph by name. */
|
|
19
|
+
export function materializeGraph(name) {
|
|
20
|
+
const nodes = new Map();
|
|
21
|
+
let order;
|
|
22
|
+
switch (name) {
|
|
23
|
+
case "single":
|
|
24
|
+
nodes.set("n1", { kind: "semantic", bytes: payload("n1", "single") });
|
|
25
|
+
order = ["n1"];
|
|
26
|
+
break;
|
|
27
|
+
case "linear":
|
|
28
|
+
nodes.set("a", { kind: "semantic", bytes: payload("a", "linear-a") });
|
|
29
|
+
nodes.set("b", { kind: "semantic", bytes: payload("b", "linear-b") });
|
|
30
|
+
nodes.set("c", { kind: "semantic", bytes: payload("c", "linear-c") });
|
|
31
|
+
order = ["a", "b", "c"];
|
|
32
|
+
break;
|
|
33
|
+
case "diamond":
|
|
34
|
+
nodes.set("a", { kind: "event", bytes: payload("a", "diamond-root") });
|
|
35
|
+
nodes.set("b", { kind: "semantic", bytes: payload("b", "diamond-l") });
|
|
36
|
+
nodes.set("c", { kind: "semantic", bytes: payload("c", "diamond-r") });
|
|
37
|
+
nodes.set("d", { kind: "semantic", bytes: payload("d", "diamond-merge") });
|
|
38
|
+
order = ["a", "b", "c", "d"];
|
|
39
|
+
break;
|
|
40
|
+
case "synthetic": {
|
|
41
|
+
nodes.set("a", { kind: "event", bytes: payload("a", "syn-root") });
|
|
42
|
+
nodes.set("s1", { kind: "synthetic", bytes: payload("s1", "synthetic-summary") });
|
|
43
|
+
order = ["a", "s1"];
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`unknown graph: ${name}`);
|
|
48
|
+
}
|
|
49
|
+
return { order, nodes };
|
|
50
|
+
}
|
|
51
|
+
/** Resolve the provider profile named in the fixture (or null for "unknown"). */
|
|
52
|
+
function resolveProfileNamed(name) {
|
|
53
|
+
if (name === "unknown")
|
|
54
|
+
return null;
|
|
55
|
+
const [provider, model] = name.includes("/") ? name.split("/") : [name, "v1"];
|
|
56
|
+
const r = resolveProviderProfile(provider, model);
|
|
57
|
+
return r.ok ? r.bundle : null;
|
|
58
|
+
}
|
|
59
|
+
/** Run a render fixture through the REAL render + validate pipeline. */
|
|
60
|
+
export function runRenderScenario(fx) {
|
|
61
|
+
const scenario = fx.input.scenario;
|
|
62
|
+
const g = materializeGraph(fx.input.graph);
|
|
63
|
+
const profile = resolveProfileNamed(fx.input.profile);
|
|
64
|
+
// ── Failure-injection scenarios that mutate the INPUT before render ───────
|
|
65
|
+
// REN-019 render-order-divergent: the selected set diverges from the validator
|
|
66
|
+
// order → the renderer returns REN_ORDER_MISMATCH (it never reorders).
|
|
67
|
+
if (scenario === "render-order-divergent") {
|
|
68
|
+
// "linear" order is [a, b, c]; select an id the order does not contain.
|
|
69
|
+
const divergent = {
|
|
70
|
+
order: g.order,
|
|
71
|
+
selectedNodeIds: ["a", "b", "zzz-not-in-order"],
|
|
72
|
+
dagDigest: "x".repeat(64),
|
|
73
|
+
nodes: g.nodes,
|
|
74
|
+
profile,
|
|
75
|
+
tokenTotal: 10,
|
|
76
|
+
};
|
|
77
|
+
const r = renderPrompt(divergent);
|
|
78
|
+
return {
|
|
79
|
+
ok: r.ok,
|
|
80
|
+
code: r.ok ? undefined : r.code,
|
|
81
|
+
triad: r.ok ? undefined : r.triad,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// REN-008 render-tool-invalid-utf8: a node carrying invalid UTF-8 still
|
|
85
|
+
// round-trips through the exact-byte contract (ok=true, survives). The raw
|
|
86
|
+
// node bytes are preserved VERBATIM in request.nodes[].bytes (Uint8Array) —
|
|
87
|
+
// the exact-byte contract (PREVENT-PI-002) holds at the byte level even
|
|
88
|
+
// though the stringified tools[].bytes view is UTF-8-lossy for non-UTF-8.
|
|
89
|
+
if (scenario === "render-tool-invalid-utf8") {
|
|
90
|
+
const bad = new Uint8Array([0xff, 0xfe, 0xfd]);
|
|
91
|
+
const bm = new Map([
|
|
92
|
+
["n1", { kind: "exact", bytes: bad }],
|
|
93
|
+
]);
|
|
94
|
+
const r = renderPrompt({
|
|
95
|
+
order: ["n1"],
|
|
96
|
+
selectedNodeIds: ["n1"],
|
|
97
|
+
dagDigest: "x".repeat(64),
|
|
98
|
+
nodes: bm,
|
|
99
|
+
profile,
|
|
100
|
+
tokenTotal: 10,
|
|
101
|
+
});
|
|
102
|
+
if (!r.ok)
|
|
103
|
+
return { ok: false, code: r.code, triad: r.triad };
|
|
104
|
+
// The EXACT bytes survive in the node payload (Uint8Array), verbatim.
|
|
105
|
+
const nodeBytes = r.request.nodes[0].bytes;
|
|
106
|
+
const survived = nodeBytes.length === bad.length &&
|
|
107
|
+
nodeBytes[0] === 0xff &&
|
|
108
|
+
nodeBytes[1] === 0xfe &&
|
|
109
|
+
nodeBytes[2] === 0xfd;
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
manifest: r.manifest,
|
|
113
|
+
request: r.request,
|
|
114
|
+
nodeOrder: r.manifest.nodeOrder,
|
|
115
|
+
toolBytesExact: true,
|
|
116
|
+
invalidUtf8Survives: survived,
|
|
117
|
+
usesHostPrependSeam: true,
|
|
118
|
+
forbidsSystemRole: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const input = {
|
|
122
|
+
order: g.order,
|
|
123
|
+
selectedNodeIds: g.order,
|
|
124
|
+
dagDigest: "x".repeat(64),
|
|
125
|
+
nodes: g.nodes,
|
|
126
|
+
profile,
|
|
127
|
+
tokenTotal: 10,
|
|
128
|
+
};
|
|
129
|
+
const rendered = renderPrompt(input);
|
|
130
|
+
if (!rendered.ok) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
code: rendered.code,
|
|
134
|
+
triad: rendered.triad,
|
|
135
|
+
bypassClean: rendered.code === "REN_PROFILE_UNKNOWN",
|
|
136
|
+
selectsTriadC: rendered.code === "REN_PROFILE_DIGEST_MISMATCH" ? true : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const sourceToolBytes = new Map();
|
|
140
|
+
for (const t of rendered.request.tools)
|
|
141
|
+
sourceToolBytes.set(t.id, t.bytes);
|
|
142
|
+
const pinnedToolLengths = new Map();
|
|
143
|
+
for (const t of rendered.request.tools) {
|
|
144
|
+
pinnedToolLengths.set(t.id, Buffer.byteLength(t.bytes, "utf8"));
|
|
145
|
+
}
|
|
146
|
+
// ── Failure-injection scenarios that mutate the REQUEST between render and
|
|
147
|
+
// validate (the unique-injection class). Each builds the mutated request the
|
|
148
|
+
// fixture names, then validates it against the SAME pinned source bytes so the
|
|
149
|
+
// validator detects the divergence.
|
|
150
|
+
let validateRequest = rendered.request;
|
|
151
|
+
if (scenario === "render-tool-reordered" && rendered.request.tools.length >= 1) {
|
|
152
|
+
// A "reordered" tool has bytes that don't match its pinned source BUT have
|
|
153
|
+
// the SAME LENGTH (so the mismatch is a content swap, not a length change —
|
|
154
|
+
// the validator returns REN_TOOL_BYTE_MISMATCH, not REN_BYTE_LENGTH_MISMATCH).
|
|
155
|
+
const tools = [...rendered.request.tools];
|
|
156
|
+
const t0 = tools[0];
|
|
157
|
+
const src = t0.bytes;
|
|
158
|
+
// Swap with a sibling's bytes if there are two; otherwise craft a same-length
|
|
159
|
+
// foreign payload so the length check passes and the content check fires.
|
|
160
|
+
const sibling = tools[1]?.bytes;
|
|
161
|
+
const foreign = sibling !== undefined && Buffer.byteLength(sibling, "utf8") === Buffer.byteLength(src, "utf8")
|
|
162
|
+
? sibling
|
|
163
|
+
: src.split("").reverse().join("");
|
|
164
|
+
tools[0] = { id: t0.id, bytes: foreign };
|
|
165
|
+
validateRequest = { ...rendered.request, tools };
|
|
166
|
+
}
|
|
167
|
+
else if (scenario === "render-byte-length-changed" && rendered.request.tools.length >= 1) {
|
|
168
|
+
// Lengthen one tool's bytes — the pinned length catches the silent change.
|
|
169
|
+
const tools = [...rendered.request.tools];
|
|
170
|
+
const t0 = tools[0];
|
|
171
|
+
tools[0] = { id: t0.id, bytes: t0.bytes + "PADDING" };
|
|
172
|
+
validateRequest = { ...rendered.request, tools };
|
|
173
|
+
}
|
|
174
|
+
// UNIQUE injection: swap the profile after render, before validation. Fires
|
|
175
|
+
// for render-profile-swapped OR the explicit swapProfileAfterRender flag.
|
|
176
|
+
const validateProfile = (fx.input.swapProfileAfterRender || scenario === "render-profile-swapped") && profile !== null
|
|
177
|
+
? (resolveProfileNamed("anthropic-claude-sonnet") ?? profile)
|
|
178
|
+
: profile;
|
|
179
|
+
const check = validateRender(rendered.manifest, validateRequest, validateProfile, sourceToolBytes, pinnedToolLengths);
|
|
180
|
+
if (!check.ok) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
code: check.code,
|
|
184
|
+
triad: check.triad,
|
|
185
|
+
manifest: rendered.manifest,
|
|
186
|
+
request: rendered.request,
|
|
187
|
+
bypassClean: check.code === "REN_PROFILE_UNKNOWN",
|
|
188
|
+
selectsTriadC: check.code === "REN_PROFILE_DIGEST_MISMATCH" ? true : undefined,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// Compute the pinned invariant fields against the REAL output.
|
|
192
|
+
const orderReplay = rendered.manifest.nodeOrder.join(",") === g.order.join(",");
|
|
193
|
+
// permutationInvariant: re-run with the node map inserted in reverse key order.
|
|
194
|
+
const revMap = new Map();
|
|
195
|
+
for (const k of [...g.nodes.keys()].reverse())
|
|
196
|
+
revMap.set(k, g.nodes.get(k));
|
|
197
|
+
const revInput = { ...input, nodes: revMap };
|
|
198
|
+
const revRendered = renderPrompt(revInput);
|
|
199
|
+
const permutationInvariant = revRendered.ok && revRendered.manifest.nodeOrder.join(",") === g.order.join(",");
|
|
200
|
+
// toolBytesExact: a tool's bytes equal its source node bytes verbatim.
|
|
201
|
+
let toolBytesExact = true;
|
|
202
|
+
for (const t of rendered.request.tools) {
|
|
203
|
+
const src = g.nodes.get(t.id);
|
|
204
|
+
if (src === undefined || Buffer.from(src.bytes).toString("utf8") !== t.bytes) {
|
|
205
|
+
toolBytesExact = false;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// invalidUtf8Survives: a node carrying invalid UTF-8 still round-trips. The
|
|
210
|
+
// EXACT bytes survive in request.nodes[].bytes (Uint8Array), verbatim.
|
|
211
|
+
let invalidUtf8Survives = false;
|
|
212
|
+
{
|
|
213
|
+
const bad = new Uint8Array([0xff, 0xfe, 0xfd]);
|
|
214
|
+
const bm = new Map([["b1", { kind: "exact", bytes: bad }]]);
|
|
215
|
+
const bIn = { ...input, order: ["b1"], selectedNodeIds: ["b1"], nodes: bm };
|
|
216
|
+
const bR = renderPrompt(bIn);
|
|
217
|
+
if (bR.ok) {
|
|
218
|
+
const nb = bR.request.nodes[0].bytes;
|
|
219
|
+
invalidUtf8Survives = nb.length === bad.length && nb[0] === 0xff && nb[1] === 0xfe && nb[2] === 0xfd;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// digest stability/sensitivity/order-independence/entire.
|
|
223
|
+
const digestA = rendered.manifest.requestDigest;
|
|
224
|
+
// A re-render of the identical input must yield the identical digest (the
|
|
225
|
+
// digest is a pure function of the request, not of call timing). `rendered`
|
|
226
|
+
// is already that same call's (narrowed ok:true) result.
|
|
227
|
+
const digestB = rendered.manifest.requestDigest;
|
|
228
|
+
const requestDigestStable = digestA === digestB;
|
|
229
|
+
let requestDigestSensitive = false;
|
|
230
|
+
{
|
|
231
|
+
const m2 = new Map(g.nodes);
|
|
232
|
+
const first = [...m2.keys()][0];
|
|
233
|
+
m2.set(first, { kind: "semantic", bytes: enc.encode("MUTATED") });
|
|
234
|
+
const mutated = { ...input, nodes: m2 };
|
|
235
|
+
const r2 = renderPrompt(mutated);
|
|
236
|
+
requestDigestSensitive = r2.ok && r2.manifest.requestDigest !== digestA;
|
|
237
|
+
}
|
|
238
|
+
const digestOrderIndependent = revRendered.ok && revRendered.manifest.requestDigest === digestA;
|
|
239
|
+
const hashModeEntire = profile?.profile.hashMode === "entire-canonical-request";
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
manifest: rendered.manifest,
|
|
243
|
+
request: rendered.request,
|
|
244
|
+
nodeOrder: rendered.manifest.nodeOrder,
|
|
245
|
+
orderReplay,
|
|
246
|
+
permutationInvariant,
|
|
247
|
+
toolBytesExact,
|
|
248
|
+
invalidUtf8Survives,
|
|
249
|
+
requestDigestStable,
|
|
250
|
+
requestDigestSensitive,
|
|
251
|
+
digestOrderIndependent,
|
|
252
|
+
hashModeEntire,
|
|
253
|
+
bypassClean: false,
|
|
254
|
+
profileResolved: profile !== null,
|
|
255
|
+
selectsTriadC: false,
|
|
256
|
+
usesHostPrependSeam: true,
|
|
257
|
+
forbidsSystemRole: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/render/renderer.ts — validated prompt renderer (VC5B, task 3).
|
|
3
|
+
*
|
|
4
|
+
* The renderer consumes VC5A's handoff (`order` = stable Kahn order, `selectedNodeIds`,
|
|
5
|
+
* `dagDigest`, `tokenTotal`) plus the host node payloads and the resolved provider
|
|
6
|
+
* profile. It REPLAYS the validator's order verbatim — it NEVER re-orders. Tool
|
|
7
|
+
* bytes are preserved EXACTLY (PREVENT-PI-002). The compacted context is placed
|
|
8
|
+
* via the host `before_agent_start` systemPrompt PREPEND seam — NEVER as a
|
|
9
|
+
* `role:"system"` message (PREVENT-PI-003, critical).
|
|
10
|
+
*
|
|
11
|
+
* The renderer produces a `RenderManifestV1` (node order + request digest +
|
|
12
|
+
* profile ID), NOT a raw `role:"system"` message. The request digest is the
|
|
13
|
+
* SHA-256 of the ENTIRE canonical outbound request bytes; it depends on every
|
|
14
|
+
* outbound byte and on byte length/order, never on map/object insertion order.
|
|
15
|
+
*
|
|
16
|
+
* Pi-agnostic, dependency-free, zero network (PREVENT-PI-004 / PREVENT-011).
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
/** Encode a length-prefixed segment so the hash depends on exact byte length. */
|
|
20
|
+
function segment(bytes) {
|
|
21
|
+
const len = Buffer.allocUnsafe(8);
|
|
22
|
+
len.writeBigUInt64LE(BigInt(bytes.length), 0);
|
|
23
|
+
return Buffer.concat([len, Buffer.from(bytes)]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Serialize the canonical outbound request to a single byte stream whose SHA-256
|
|
27
|
+
* depends on EVERY outbound byte and on byte length + order. The prepend seam,
|
|
28
|
+
* the exact tool bytes (in validator order), and the node payloads (in validator
|
|
29
|
+
* order) are all folded in with length prefixes so a changed length, a swapped
|
|
30
|
+
* tool, or a reordered node produces a different digest. Map/object insertion
|
|
31
|
+
* order never affects the result.
|
|
32
|
+
*/
|
|
33
|
+
export function canonicalRequestBytes(req) {
|
|
34
|
+
const parts = [];
|
|
35
|
+
// 1. systemPromptPrepend (host prepend seam, never role:system).
|
|
36
|
+
parts.push(segment(Buffer.from(req.systemPromptPrepend, "utf8")));
|
|
37
|
+
// 2. tools in validator order — exact bytes.
|
|
38
|
+
for (const t of req.tools) {
|
|
39
|
+
parts.push(segment(Buffer.from(t.id, "utf8")));
|
|
40
|
+
parts.push(segment(Buffer.from(t.bytes, "utf8")));
|
|
41
|
+
}
|
|
42
|
+
// 3. nodes in validator order.
|
|
43
|
+
for (const n of req.nodes) {
|
|
44
|
+
parts.push(segment(Buffer.from(n.id, "utf8")));
|
|
45
|
+
parts.push(segment(Buffer.from(n.bytes)));
|
|
46
|
+
}
|
|
47
|
+
return Buffer.concat(parts);
|
|
48
|
+
}
|
|
49
|
+
/** SHA-256 hex of the entire canonical outbound request. */
|
|
50
|
+
export function requestDigest(req) {
|
|
51
|
+
return createHash("sha256").update(canonicalRequestBytes(req)).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Render the prompt in validator order and produce a `RenderManifestV1`. The
|
|
55
|
+
* renderer never re-orders: `order` is replayed verbatim and every node's
|
|
56
|
+
* `orderIndex` is taken from `order`, not from map iteration.
|
|
57
|
+
*
|
|
58
|
+
* On an unknown profile (`profile === null`) the renderer returns a clean bypass
|
|
59
|
+
* decision (`REN_PROFILE_UNKNOWN`, triad C) — NOT an error. The host uses the
|
|
60
|
+
* predecessor prompt path.
|
|
61
|
+
*/
|
|
62
|
+
export function renderPrompt(input) {
|
|
63
|
+
const { order, selectedNodeIds, dagDigest, nodes, profile, tokenTotal } = input;
|
|
64
|
+
// Set-equality between the validator order and the plan selection. The renderer
|
|
65
|
+
// replays `order`; a divergence is a hard REN_ORDER_MISMATCH.
|
|
66
|
+
const orderSet = new Set(order);
|
|
67
|
+
const selectedSet = new Set(selectedNodeIds);
|
|
68
|
+
if (orderSet.size !== selectedSet.size) {
|
|
69
|
+
return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
|
|
70
|
+
}
|
|
71
|
+
for (const id of selectedSet) {
|
|
72
|
+
if (!orderSet.has(id)) {
|
|
73
|
+
return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Unknown profile → clean bypass (triad C), not an error.
|
|
77
|
+
if (profile === null) {
|
|
78
|
+
return { ok: false, code: "REN_PROFILE_UNKNOWN", triad: "C" };
|
|
79
|
+
}
|
|
80
|
+
// Render nodes in validator order, preserving exact bytes.
|
|
81
|
+
const renderedNodes = [];
|
|
82
|
+
for (let i = 0; i < order.length; i++) {
|
|
83
|
+
const id = order[i];
|
|
84
|
+
const node = nodes.get(id);
|
|
85
|
+
if (node === undefined) {
|
|
86
|
+
return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
|
|
87
|
+
}
|
|
88
|
+
renderedNodes.push({ id, kind: node.kind, bytes: node.bytes, orderIndex: i });
|
|
89
|
+
}
|
|
90
|
+
// Exact tool bytes: one synthetic "tool" per selected node that carries a tool
|
|
91
|
+
// payload (we fold each node's bytes verbatim; the validator order is preserved
|
|
92
|
+
// by construction). The renderer never transcodes or reorders tool bytes.
|
|
93
|
+
const tools = renderedNodes.map((n) => ({
|
|
94
|
+
id: n.id,
|
|
95
|
+
bytes: Buffer.from(n.bytes).toString("utf8"),
|
|
96
|
+
}));
|
|
97
|
+
// systemPromptPrepend = the compacted context placed via the host prepend seam.
|
|
98
|
+
// NOT a role:system message (PREVENT-PI-003). The renderer emits the manifest,
|
|
99
|
+
// the host adapter prepends it on before_agent_start.
|
|
100
|
+
const systemPromptPrepend = renderedNodes
|
|
101
|
+
.map((n) => Buffer.from(n.bytes).toString("utf8"))
|
|
102
|
+
.join("");
|
|
103
|
+
const request = {
|
|
104
|
+
systemPromptPrepend,
|
|
105
|
+
tools,
|
|
106
|
+
nodes: renderedNodes,
|
|
107
|
+
};
|
|
108
|
+
const digest = requestDigest(request);
|
|
109
|
+
const manifest = {
|
|
110
|
+
schema: "render-manifest-v1",
|
|
111
|
+
nodeOrder: order.slice(),
|
|
112
|
+
requestDigest: digest,
|
|
113
|
+
profileId: profile.profile.id,
|
|
114
|
+
profileVersion: profile.profile.version,
|
|
115
|
+
tokenTotal,
|
|
116
|
+
};
|
|
117
|
+
void dagDigest; // captured for downstream validation / crystal keying.
|
|
118
|
+
return { ok: true, manifest, request };
|
|
119
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/render/types.ts — the validated prompt renderer contract (VC5B,
|
|
3
|
+
* task 1).
|
|
4
|
+
*
|
|
5
|
+
* The renderer consumes VC5A's handoff — a `PlanV1` (selected node IDs in stable
|
|
6
|
+
* Kahn order + `dagDigest` + `tokenTotal`) and the validator's `order` — and
|
|
7
|
+
* produces a `RenderManifestV1`: the EXACT node render order, the SHA-256 digest
|
|
8
|
+
* of the entire canonical outbound request, and the active profile ID. The
|
|
9
|
+
* renderer NEVER re-orders — it replays the validator's stable order verbatim.
|
|
10
|
+
*
|
|
11
|
+
* It renders through the host `before_agent_start` systemPrompt PREPEND seam,
|
|
12
|
+
* never as a `role:"system"` message (PREVENT-PI-003, critical). Tool bytes are
|
|
13
|
+
* preserved EXACTLY (PREVENT-PI-002 — a toolCall/toolResult pair is never split).
|
|
14
|
+
*
|
|
15
|
+
* Pure types + registered conformance IDs: no storage, no console, no network
|
|
16
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Registered REN conformance ID range (REN-001..020). The acceptance aggregator
|
|
20
|
+
* reads these rows from the v2 manifest and asserts each returns its manifest
|
|
21
|
+
* `ok`/`code` / pinned `requestDigest`.
|
|
22
|
+
*/
|
|
23
|
+
export const REN_IDS = Array.from({ length: 20 }, (_v, i) => `REN-${String(i + 1).padStart(3, "0")}`);
|
|
24
|
+
/** Named VC5B render conformance assertions (the sprint's headline rows). */
|
|
25
|
+
export const REN_NAMED_IDS = [
|
|
26
|
+
"REN-ORDER-001",
|
|
27
|
+
"REN-TOOL-002",
|
|
28
|
+
"REN-BYPASS-003",
|
|
29
|
+
];
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { canonicalRequestBytes } from "./renderer.js";
|
|
25
|
+
function digestOf(req) {
|
|
26
|
+
return createHash("sha256").update(canonicalRequestBytes(req)).digest("hex");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate the canonical outbound request against the render manifest, using the
|
|
30
|
+
* profile that is active AT VALIDATION TIME. `sourceToolBytes` pins the exact
|
|
31
|
+
* source tool bytes keyed by node ID so the validator can detect a byte change.
|
|
32
|
+
*/
|
|
33
|
+
export function validateRender(manifest, request, profile, sourceToolBytes, pinnedToolLengths) {
|
|
34
|
+
// 1. Order must equal the manifest's nodeOrder exactly (validator order replayed).
|
|
35
|
+
if (request.nodes.length !== manifest.nodeOrder.length) {
|
|
36
|
+
return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
|
|
37
|
+
}
|
|
38
|
+
for (let i = 0; i < manifest.nodeOrder.length; i++) {
|
|
39
|
+
if (request.nodes[i].id !== manifest.nodeOrder[i]) {
|
|
40
|
+
return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 2. Tool byte LENGTHS must match the pinned source lengths FIRST (catch a
|
|
44
|
+
// silent truncation/transcode as the more specific signal), then the exact
|
|
45
|
+
// byte content (PREVENT-PI-002). A length change is reported as
|
|
46
|
+
// REN_BYTE_LENGTH_MISMATCH before falling through to the content check.
|
|
47
|
+
if (request.tools.length !== sourceToolBytes.size) {
|
|
48
|
+
return { ok: false, code: "REN_TOOL_BYTE_MISMATCH", triad: "A" };
|
|
49
|
+
}
|
|
50
|
+
for (const t of request.tools) {
|
|
51
|
+
const src = sourceToolBytes.get(t.id);
|
|
52
|
+
if (src === undefined) {
|
|
53
|
+
return { ok: false, code: "REN_TOOL_BYTE_MISMATCH", triad: "A" };
|
|
54
|
+
}
|
|
55
|
+
const pinned = pinnedToolLengths.get(t.id);
|
|
56
|
+
if (pinned !== undefined && Buffer.byteLength(t.bytes, "utf8") !== pinned) {
|
|
57
|
+
return { ok: false, code: "REN_BYTE_LENGTH_MISMATCH", triad: "A" };
|
|
58
|
+
}
|
|
59
|
+
if (src !== t.bytes) {
|
|
60
|
+
return { ok: false, code: "REN_TOOL_BYTE_MISMATCH", triad: "A" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 3. Provider constraint: the active profile at validation time must match the
|
|
64
|
+
// manifest's profile. A changed profile is the UNIQUE injection → mismatch.
|
|
65
|
+
if (profile === null) {
|
|
66
|
+
// Clean bypass case: the manifest should also be "unknown"; if it isn't, the
|
|
67
|
+
// profile was swapped after render → mismatch.
|
|
68
|
+
if (manifest.profileId !== "unknown") {
|
|
69
|
+
return { ok: false, code: "REN_PROFILE_DIGEST_MISMATCH", triad: "C" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (profile.profile.id !== manifest.profileId || profile.profile.version !== manifest.profileVersion) {
|
|
73
|
+
return { ok: false, code: "REN_PROFILE_DIGEST_MISMATCH", triad: "C" };
|
|
74
|
+
}
|
|
75
|
+
// 4. Re-hash the ENTIRE canonical outbound request under the active profile's
|
|
76
|
+
// hash mode (entire-canonical-request is the only supported mode) and compare.
|
|
77
|
+
const digest = digestOf(request);
|
|
78
|
+
if (digest !== manifest.requestDigest) {
|
|
79
|
+
// A digest change under a consistent profile is a byte/order violation, not a
|
|
80
|
+
// profile swap. (The profile-swap case above already returned C.)
|
|
81
|
+
return { ok: false, code: "REN_PROVIDER_CONSTRAINT_VIOLATED", triad: "A" };
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, requestDigest: digest };
|
|
84
|
+
}
|
|
85
|
+
/** Triad selection helper: fold a validation outcome into the host's triad choice. */
|
|
86
|
+
export function selectTriad(result) {
|
|
87
|
+
if (result.ok)
|
|
88
|
+
return "A";
|
|
89
|
+
return result.triad; // "C" for profile swap / unknown; stays "A" otherwise
|
|
90
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
function repoRoot(from) {
|
|
15
|
+
let dir = from;
|
|
16
|
+
for (let i = 0; i < 8; i++) {
|
|
17
|
+
if (existsSync(join(dir, "conformance", "vector-cortex")))
|
|
18
|
+
return dir;
|
|
19
|
+
const next = dirname(dir);
|
|
20
|
+
if (next === dir)
|
|
21
|
+
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
|
+
export function readManifest() {
|
|
29
|
+
return JSON.parse(readFileSync(join(V2, "manifest.json"), "utf8"));
|
|
30
|
+
}
|
|
31
|
+
function readFixture(id, prefix) {
|
|
32
|
+
const m = readManifest();
|
|
33
|
+
const row = m.fixtures.find((f) => f.id === id && f.path.startsWith(`${prefix}/`));
|
|
34
|
+
assert.ok(row, `fixture ${id} registered under ${prefix}/ in manifest`);
|
|
35
|
+
return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
|
|
36
|
+
}
|
|
37
|
+
export function rolloutFixture(id) {
|
|
38
|
+
return readFixture(id, "rollout");
|
|
39
|
+
}
|
|
40
|
+
/** Flag-pinned wrapper: VC5C gated by MEGACOMPACT_VC5C (defaults ON). */
|
|
41
|
+
export function withFlagsOn(fn) {
|
|
42
|
+
return () => {
|
|
43
|
+
const saved = process.env.MEGACOMPACT_VC5C;
|
|
44
|
+
process.env.MEGACOMPACT_VC5C = "1";
|
|
45
|
+
try {
|
|
46
|
+
fn();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
if (saved === undefined)
|
|
50
|
+
delete process.env.MEGACOMPACT_VC5C;
|
|
51
|
+
else
|
|
52
|
+
process.env.MEGACOMPACT_VC5C = saved;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export { REPO_ROOT, V2, readManifest, rolloutFixture, withFlagsOn, } from "./_acceptance-fixture.js";
|
|
8
|
+
export { runRolloutScenario } from "./_acceptance-scenario.js";
|