pi-mega-compact 0.20.3 → 0.20.5
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 +12 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-render.js +40 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +29 -22
- package/dist/extensions/mega-context/vector-cortex.js +128 -0
- package/dist/src/config/vector-cortex.js +12 -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/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/extensions/dashboard-client/dist/assets/{AreaChart-Cam9-nhT.js → AreaChart-CGL29Tcl.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-Cam9-nhT.js.map → AreaChart-CGL29Tcl.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-DzIY8aM1.js → BarChart-OxhalIZa.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-DzIY8aM1.js.map → BarChart-OxhalIZa.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CKIiYsG_.js → CacheTab-VcMFwL-6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CKIiYsG_.js.map → CacheTab-VcMFwL-6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-xaaBJLiK.js → EventsTab-DePofXDM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-xaaBJLiK.js.map → EventsTab-DePofXDM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-BGtUKOqG.js → HealthTab-nQd1tjSF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-BGtUKOqG.js.map → HealthTab-nQd1tjSF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ABgQnTZ9.js → MaintenanceTab-CzvA8-F6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ABgQnTZ9.js.map → MaintenanceTab-CzvA8-F6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-ChR2GAN-.js → MemoryMapTab-3lxgWBvU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-ChR2GAN-.js.map → MemoryMapTab-3lxgWBvU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DdCLAgcR.js → MetricsTab-D_FcA6kN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DdCLAgcR.js.map → MetricsTab-D_FcA6kN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DtemzJt6.js → OverviewTab-BU6R4aEs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DtemzJt6.js.map → OverviewTab-BU6R4aEs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-B9mvD9Ep.js → ReposTab-DFjy1isz.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-B9mvD9Ep.js.map → ReposTab-DFjy1isz.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcO38XLS.js → SessionsTab-UGIzIH25.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BcO38XLS.js.map → SessionsTab-UGIzIH25.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-D_TBNP09.js → SetupTab-CECrgMKI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-D_TBNP09.js.map → SetupTab-CECrgMKI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-TjHpbUOo.js → TimeSavedCard-Dyy4SCDk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-TjHpbUOo.js.map → TimeSavedCard-Dyy4SCDk.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DprxndCF.js → TurnsTab-B0LPcVs1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DprxndCF.js.map → TurnsTab-B0LPcVs1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BWi3u_gp.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BWi3u_gp.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-OKgSo15G.js → WikiTab-DWk9Qvut.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-OKgSo15G.js.map → WikiTab-DWk9Qvut.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-D_Qz4q7L.js → button-O59wPr5H.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-D_Qz4q7L.js.map → button-O59wPr5H.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-CsI6Jm3p.js → card-C-DxXv1-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-CsI6Jm3p.js.map → card-C-DxXv1-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CvHKUdhX.js → generateCategoricalChart-Dw5YnE5s.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CvHKUdhX.js.map → generateCategoricalChart-Dw5YnE5s.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-0Ye_un22.js → index-i9oaPE0a.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-0Ye_un22.js.map → index-i9oaPE0a.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-VQEoJl8Q.js → switch-B69QY3IM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-VQEoJl8Q.js.map → switch-B69QY3IM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-CAPm4HJo.js → toggle-BJ4Kwggu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-CAPm4HJo.js.map → toggle-BJ4Kwggu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-pxsNTVkh.js → useSSE-BpuA8sPd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-pxsNTVkh.js.map → useSSE-BpuA8sPd.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -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/VectorCortexTab.tsx +11 -38
- package/extensions/dashboard-client/src/types/vector-cortex.ts +9 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +20 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-render.ts +49 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +1 -0
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +31 -22
- package/extensions/mega-context/vector-cortex.ts +163 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +13 -0
- package/src/config.ts +1 -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/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,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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/provider/registry.ts — fixture-backed base provider profile
|
|
3
|
+
* registry (VC5B, task 2).
|
|
4
|
+
*
|
|
5
|
+
* The registry resolves a `(provider, model)` pair to a `ProviderProfileBundle`
|
|
6
|
+
* (CONTRACTS §ProviderProfileV1 + role/tool/cache rules). Unknown provider/model
|
|
7
|
+
* returns `PRO_PROFILE_UNKNOWN` and the renderer CLEANLY bypasses vector-cortex
|
|
8
|
+
* rendering (NOT an error — the host uses the predecessor prompt path).
|
|
9
|
+
*
|
|
10
|
+
* Profiles are fixture-backed: each base profile is declared here as a constant
|
|
11
|
+
* and is also pinned as a conformance fixture under `conformance/vector-cortex/v2/
|
|
12
|
+
* provider/` (registered by `scripts/gen-fixtures/provider.mjs`). The registry is
|
|
13
|
+
* the REAL resolver the acceptance suite drives; the fixtures are the committed
|
|
14
|
+
* canonical corpus that proves each profile's cache-identity behavior.
|
|
15
|
+
*
|
|
16
|
+
* Pi-agnostic, dependency-free, zero network (PREVENT-PI-004 / PREVENT-011).
|
|
17
|
+
*/
|
|
18
|
+
import { PRO_PROFILE_UNKNOWN, } from "./types.js";
|
|
19
|
+
/** A registry key is `provider` + `model`. Lookup is exact-match. */
|
|
20
|
+
function key(provider, model) {
|
|
21
|
+
return `${provider}//${model}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The base role rule every VC5B profile shares: the renderer MUST use the host
|
|
25
|
+
* `before_agent_start` prepend seam and MUST NOT inject `role:"system"`
|
|
26
|
+
* (PREVENT-PI-003, critical).
|
|
27
|
+
*/
|
|
28
|
+
const BASE_ROLE = {
|
|
29
|
+
useHostPrependSeam: true,
|
|
30
|
+
forbidSystemRoleInjection: true,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The base tool rule every VC5B profile shares: exact tool bytes preserved
|
|
34
|
+
* (PREVENT-PI-002) and the renderer never reorders away from the validator order.
|
|
35
|
+
*/
|
|
36
|
+
const BASE_TOOL = {
|
|
37
|
+
preserveExactToolBytes: true,
|
|
38
|
+
forbidToolReorder: true,
|
|
39
|
+
};
|
|
40
|
+
/** Build a base cache rule (entire-canonical-request hashing by default). */
|
|
41
|
+
function baseCache(stableFields) {
|
|
42
|
+
return {
|
|
43
|
+
hashMode: "entire-canonical-request",
|
|
44
|
+
stableFields,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Build a contract-profile shape pinned as a conformance fixture too. */
|
|
48
|
+
function profile(id, version, excludedJsonPointers) {
|
|
49
|
+
return {
|
|
50
|
+
schema: "provider-profile-v1",
|
|
51
|
+
id,
|
|
52
|
+
version,
|
|
53
|
+
hashMode: "entire-canonical-request",
|
|
54
|
+
excludedJsonPointers,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The fixture-backed base profiles. Each entry is the REAL bundle the renderer
|
|
59
|
+
* resolves; the parallel conformance fixtures prove the cache-identity behavior.
|
|
60
|
+
*/
|
|
61
|
+
const BASE_PROFILES = [
|
|
62
|
+
{
|
|
63
|
+
profile: profile("anthropic-claude-opus", "v1", []),
|
|
64
|
+
role: BASE_ROLE,
|
|
65
|
+
tool: BASE_TOOL,
|
|
66
|
+
cache: baseCache(["systemPromptPrepend", "tools", "nodes"]),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
profile: profile("anthropic-claude-sonnet", "v1", []),
|
|
70
|
+
role: BASE_ROLE,
|
|
71
|
+
tool: BASE_TOOL,
|
|
72
|
+
cache: baseCache(["systemPromptPrepend", "tools", "nodes"]),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
profile: profile("openai-gpt", "v1", []),
|
|
76
|
+
role: BASE_ROLE,
|
|
77
|
+
tool: BASE_TOOL,
|
|
78
|
+
cache: baseCache(["systemPromptPrepend", "tools", "nodes"]),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// A profile that proves a fixture-excluded pointer: a provider whose
|
|
82
|
+
// request-id header cannot affect cache identity. Excluded + versioned.
|
|
83
|
+
profile: profile("google-gemini", "v1", [
|
|
84
|
+
{
|
|
85
|
+
pointer: "/requestId",
|
|
86
|
+
fixtureId: "PRO-EXCLUDE-010",
|
|
87
|
+
proofDigest: "sha256:excluded-request-id-proof",
|
|
88
|
+
},
|
|
89
|
+
]),
|
|
90
|
+
role: BASE_ROLE,
|
|
91
|
+
tool: BASE_TOOL,
|
|
92
|
+
cache: baseCache(["systemPromptPrepend", "tools", "nodes"]),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
const REGISTRY = new Map(BASE_PROFILES.map((b) => [key(b.profile.id, b.profile.version), b]));
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a `(provider, model)` pair to a `ProviderProfileBundle`. Unknown
|
|
98
|
+
* provider / model / version → a CLEAN bypass (`PRO_PROFILE_UNKNOWN`), never an
|
|
99
|
+
* error. The renderer consults this and, on `ok:false`, selects triad C (the
|
|
100
|
+
* predecessor prompt path).
|
|
101
|
+
*/
|
|
102
|
+
export function resolveProviderProfile(provider, model) {
|
|
103
|
+
const bundle = REGISTRY.get(key(provider, model));
|
|
104
|
+
if (bundle === undefined) {
|
|
105
|
+
return { ok: false, code: PRO_PROFILE_UNKNOWN };
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, bundle };
|
|
108
|
+
}
|
|
109
|
+
/** The fixture-backed base profiles (exposed for tests / dashboard introspection). */
|
|
110
|
+
export const BASE_PROVIDER_PROFILES = BASE_PROFILES;
|
|
111
|
+
/** The set of known `(provider, model)` keys, for conformance registration. */
|
|
112
|
+
export const KNOWN_PROVIDER_KEYS = Array.from(REGISTRY.keys());
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/provider/types.ts — provider profile contract (VC5B, task 1).
|
|
3
|
+
*
|
|
4
|
+
* A `ProviderProfileV1` describes how a given provider/model caches and what the
|
|
5
|
+
* renderer may or may not fold into the outbound prompt without breaking the
|
|
6
|
+
* provider's KV-cache identity (CONTRACTS §ProviderProfileV1 + crystals).
|
|
7
|
+
*
|
|
8
|
+
* The renderer hashes the ENTIRE canonical outbound request by default
|
|
9
|
+
* (`hashMode: "entire-canonical-request"`). A profile may EXCLUDE a field only
|
|
10
|
+
* when a fixture proves that field cannot affect provider cache identity; every
|
|
11
|
+
* exclusion is versioned and listed in the manifest. Unknown provider / profile /
|
|
12
|
+
* version → the renderer bypasses vector-cortex rendering (a clean bypass, NOT an
|
|
13
|
+
* error) and the host falls back to the predecessor prompt path.
|
|
14
|
+
*
|
|
15
|
+
* Pure types + registered conformance IDs: no storage, no console, no network
|
|
16
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The sentinel returned for an unknown provider/model. The renderer sees this as
|
|
20
|
+
* a CLEAN bypass decision, not an error, and emits `vector_cortex_provider_bypassed`.
|
|
21
|
+
*/
|
|
22
|
+
export const PRO_PROFILE_UNKNOWN = "PRO_PROFILE_UNKNOWN";
|
|
23
|
+
/**
|
|
24
|
+
* Registered PRO conformance ID range (PRO-001..015). The acceptance aggregator
|
|
25
|
+
* reads these rows from the v2 manifest and asserts each returns its manifest
|
|
26
|
+
* `ok`/`code` / pinned `requestDigest`.
|
|
27
|
+
*/
|
|
28
|
+
export const PRO_IDS = Array.from({ length: 15 }, (_v, i) => `PRO-${String(i + 1).padStart(3, "0")}`);
|
|
29
|
+
/** Named VC5B provider conformance assertions (the sprint's headline rows). */
|
|
30
|
+
export const PRO_NAMED_IDS = ["PRO-UNKNOWN-003"];
|