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.
Files changed (130) hide show
  1. package/dist/config/vector-cortex.js +24 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +2 -0
  4. package/dist/extensions/dashboard-server/routes-vector-cortex-render.js +40 -0
  5. package/dist/extensions/dashboard-server/routes-vector-cortex-rollout.js +43 -0
  6. package/dist/extensions/dashboard-server/routes-vector-cortex.js +2 -0
  7. package/dist/extensions/dashboard-server/routes.js +1 -1
  8. package/dist/extensions/dashboard-server/server.js +5 -1
  9. package/dist/extensions/mega-context/vector-cortex.js +128 -0
  10. package/dist/extensions/mega-runtime/vector-cortex-live.js +105 -0
  11. package/dist/src/config/vector-cortex.js +24 -0
  12. package/dist/src/config.js +1 -1
  13. package/dist/src/store/sqlite/global-sessions.js +16 -4
  14. package/dist/src/vector-cortex/provider/registry.js +112 -0
  15. package/dist/src/vector-cortex/provider/types.js +30 -0
  16. package/dist/src/vector-cortex/render/_acceptance-fixture.js +58 -0
  17. package/dist/src/vector-cortex/render/_acceptance-helpers.js +17 -0
  18. package/dist/src/vector-cortex/render/_acceptance-provider.js +29 -0
  19. package/dist/src/vector-cortex/render/_acceptance-scenario.js +259 -0
  20. package/dist/src/vector-cortex/render/renderer.js +119 -0
  21. package/dist/src/vector-cortex/render/types.js +29 -0
  22. package/dist/src/vector-cortex/render/validator.js +90 -0
  23. package/dist/src/vector-cortex/rollout/_acceptance-fixture.js +55 -0
  24. package/dist/src/vector-cortex/rollout/_acceptance-helpers.js +8 -0
  25. package/dist/src/vector-cortex/rollout/_acceptance-scenario.js +70 -0
  26. package/dist/src/vector-cortex/rollout/assign.js +56 -0
  27. package/dist/src/vector-cortex/rollout/emit.js +50 -0
  28. package/dist/src/vector-cortex/rollout/gate.js +75 -0
  29. package/dist/src/vector-cortex/rollout/types.js +39 -0
  30. package/dist/vector-cortex/provider/registry.js +112 -0
  31. package/dist/vector-cortex/provider/types.js +30 -0
  32. package/dist/vector-cortex/render/_acceptance-fixture.js +58 -0
  33. package/dist/vector-cortex/render/_acceptance-helpers.js +17 -0
  34. package/dist/vector-cortex/render/_acceptance-provider.js +29 -0
  35. package/dist/vector-cortex/render/_acceptance-scenario.js +259 -0
  36. package/dist/vector-cortex/render/renderer.js +119 -0
  37. package/dist/vector-cortex/render/types.js +29 -0
  38. package/dist/vector-cortex/render/validator.js +90 -0
  39. package/dist/vector-cortex/rollout/_acceptance-fixture.js +55 -0
  40. package/dist/vector-cortex/rollout/_acceptance-helpers.js +8 -0
  41. package/dist/vector-cortex/rollout/_acceptance-scenario.js +70 -0
  42. package/dist/vector-cortex/rollout/assign.js +56 -0
  43. package/dist/vector-cortex/rollout/emit.js +50 -0
  44. package/dist/vector-cortex/rollout/gate.js +75 -0
  45. package/dist/vector-cortex/rollout/types.js +39 -0
  46. package/extensions/dashboard-client/dist/assets/{AreaChart-Cam9-nhT.js → AreaChart-DqTbaSRA.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{AreaChart-Cam9-nhT.js.map → AreaChart-DqTbaSRA.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/{BarChart-DzIY8aM1.js → BarChart-BfNBeOvF.js} +2 -2
  49. package/extensions/dashboard-client/dist/assets/{BarChart-DzIY8aM1.js.map → BarChart-BfNBeOvF.js.map} +1 -1
  50. package/extensions/dashboard-client/dist/assets/{CacheTab-CKIiYsG_.js → CacheTab-C9SOMYB5.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{CacheTab-CKIiYsG_.js.map → CacheTab-C9SOMYB5.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{EventsTab-xaaBJLiK.js → EventsTab-GsMKP10_.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{EventsTab-xaaBJLiK.js.map → EventsTab-GsMKP10_.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{HealthTab-BGtUKOqG.js → HealthTab-DAOCXwRP.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{HealthTab-BGtUKOqG.js.map → HealthTab-DAOCXwRP.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ABgQnTZ9.js → MaintenanceTab-BtBeMkB5.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-ABgQnTZ9.js.map → MaintenanceTab-BtBeMkB5.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-ChR2GAN-.js → MemoryMapTab-DzndtgkZ.js} +2 -2
  59. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-ChR2GAN-.js.map → MemoryMapTab-DzndtgkZ.js.map} +1 -1
  60. package/extensions/dashboard-client/dist/assets/{MetricsTab-DdCLAgcR.js → MetricsTab-DyxJ4eb8.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{MetricsTab-DdCLAgcR.js.map → MetricsTab-DyxJ4eb8.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{OverviewTab-DtemzJt6.js → OverviewTab-DCiEdVWR.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{OverviewTab-DtemzJt6.js.map → OverviewTab-DCiEdVWR.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{ReposTab-B9mvD9Ep.js → ReposTab-B5SIqVHA.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{ReposTab-B9mvD9Ep.js.map → ReposTab-B5SIqVHA.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{SessionsTab-BcO38XLS.js → SessionsTab-DLWFJ9xI.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{SessionsTab-BcO38XLS.js.map → SessionsTab-DLWFJ9xI.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/{SetupTab-D_TBNP09.js → SetupTab-CvGXZyej.js} +2 -2
  69. package/extensions/dashboard-client/dist/assets/{SetupTab-D_TBNP09.js.map → SetupTab-CvGXZyej.js.map} +1 -1
  70. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-TjHpbUOo.js → TimeSavedCard-wZuQGxLS.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-TjHpbUOo.js.map → TimeSavedCard-wZuQGxLS.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{TurnsTab-DprxndCF.js → TurnsTab-B7aOejch.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{TurnsTab-DprxndCF.js.map → TurnsTab-B7aOejch.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js +2 -0
  75. package/extensions/dashboard-client/dist/assets/VectorCortexTab-eHVt9yDx.js.map +1 -0
  76. package/extensions/dashboard-client/dist/assets/{WikiTab-OKgSo15G.js → WikiTab-DhWHjAtt.js} +2 -2
  77. package/extensions/dashboard-client/dist/assets/{WikiTab-OKgSo15G.js.map → WikiTab-DhWHjAtt.js.map} +1 -1
  78. package/extensions/dashboard-client/dist/assets/{button-D_Qz4q7L.js → button-BrC-W87h.js} +2 -2
  79. package/extensions/dashboard-client/dist/assets/{button-D_Qz4q7L.js.map → button-BrC-W87h.js.map} +1 -1
  80. package/extensions/dashboard-client/dist/assets/{card-CsI6Jm3p.js → card-0rgfRgr3.js} +2 -2
  81. package/extensions/dashboard-client/dist/assets/{card-CsI6Jm3p.js.map → card-0rgfRgr3.js.map} +1 -1
  82. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CvHKUdhX.js → generateCategoricalChart-DRresOQF.js} +2 -2
  83. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-CvHKUdhX.js.map → generateCategoricalChart-DRresOQF.js.map} +1 -1
  84. package/extensions/dashboard-client/dist/assets/{index-0Ye_un22.js → index-DohmtZWL.js} +3 -3
  85. package/extensions/dashboard-client/dist/assets/{index-0Ye_un22.js.map → index-DohmtZWL.js.map} +1 -1
  86. package/extensions/dashboard-client/dist/assets/{switch-VQEoJl8Q.js → switch-Bs8g_zB5.js} +2 -2
  87. package/extensions/dashboard-client/dist/assets/{switch-VQEoJl8Q.js.map → switch-Bs8g_zB5.js.map} +1 -1
  88. package/extensions/dashboard-client/dist/assets/{toggle-CAPm4HJo.js → toggle-DVVauIo8.js} +2 -2
  89. package/extensions/dashboard-client/dist/assets/{toggle-CAPm4HJo.js.map → toggle-DVVauIo8.js.map} +1 -1
  90. package/extensions/dashboard-client/dist/assets/{useSSE-pxsNTVkh.js → useSSE-DxH1PTAR.js} +2 -2
  91. package/extensions/dashboard-client/dist/assets/{useSSE-pxsNTVkh.js.map → useSSE-DxH1PTAR.js.map} +1 -1
  92. package/extensions/dashboard-client/dist/index.html +1 -1
  93. package/extensions/dashboard-client/src/api/vector-cortex.ts +18 -0
  94. package/extensions/dashboard-client/src/tabs/VectorCortexMetric.tsx +10 -0
  95. package/extensions/dashboard-client/src/tabs/VectorCortexPlansCard.tsx +39 -0
  96. package/extensions/dashboard-client/src/tabs/VectorCortexRenderCard.tsx +38 -0
  97. package/extensions/dashboard-client/src/tabs/VectorCortexRolloutCard.tsx +44 -0
  98. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +19 -38
  99. package/extensions/dashboard-client/src/types/vector-cortex.ts +26 -0
  100. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +52 -1
  101. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +12 -0
  102. package/extensions/dashboard-server/routes-vector-cortex-render.ts +49 -0
  103. package/extensions/dashboard-server/routes-vector-cortex-rollout.ts +52 -0
  104. package/extensions/dashboard-server/routes-vector-cortex.ts +2 -0
  105. package/extensions/dashboard-server/routes.ts +2 -0
  106. package/extensions/dashboard-server/server.ts +4 -0
  107. package/extensions/mega-context/vector-cortex.ts +163 -0
  108. package/extensions/mega-runtime/vector-cortex-live.ts +165 -0
  109. package/package.json +1 -1
  110. package/src/config/vector-cortex.ts +26 -0
  111. package/src/config.ts +2 -0
  112. package/src/store/sqlite/global-sessions.ts +14 -2
  113. package/src/vector-cortex/provider/registry.ts +137 -0
  114. package/src/vector-cortex/provider/types.ts +163 -0
  115. package/src/vector-cortex/render/_acceptance-fixture.ts +128 -0
  116. package/src/vector-cortex/render/_acceptance-helpers.ts +39 -0
  117. package/src/vector-cortex/render/_acceptance-provider.ts +45 -0
  118. package/src/vector-cortex/render/_acceptance-scenario.ts +319 -0
  119. package/src/vector-cortex/render/renderer.ts +142 -0
  120. package/src/vector-cortex/render/types.ts +174 -0
  121. package/src/vector-cortex/render/validator.ts +120 -0
  122. package/src/vector-cortex/rollout/_acceptance-fixture.ts +94 -0
  123. package/src/vector-cortex/rollout/_acceptance-helpers.ts +24 -0
  124. package/src/vector-cortex/rollout/_acceptance-scenario.ts +96 -0
  125. package/src/vector-cortex/rollout/assign.ts +67 -0
  126. package/src/vector-cortex/rollout/emit.ts +81 -0
  127. package/src/vector-cortex/rollout/gate.ts +119 -0
  128. package/src/vector-cortex/rollout/types.ts +111 -0
  129. package/extensions/dashboard-client/dist/assets/VectorCortexTab-Bym21yVQ.js +0 -2
  130. 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,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";
@@ -0,0 +1,70 @@
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
+ import { assignSession } from "./assign.js";
12
+ import { decideGate, } from "./gate.js";
13
+ const GATE_MIN_ELAPSED_MS = 72 * 60 * 60 * 1000;
14
+ /** Fixed monotonic clock: elapsed = now - windowStart (the only driver). */
15
+ function clockAt(elapsedMs, windowStartMs) {
16
+ return {
17
+ now: () => windowStartMs + elapsedMs, // monotonic delta is the truth
18
+ wallNow: () => new Date(1_700_000_000_000 + elapsedMs).toISOString(),
19
+ };
20
+ }
21
+ /** Run a rollout fixture through the REAL assign + gate pipeline. */
22
+ export function runRolloutScenario(fx) {
23
+ const scenario = fx.input.scenario;
24
+ if (scenario === "assign-stable") {
25
+ const a = assignSession(fx.input.sessionId ?? "");
26
+ const b = assignSession(fx.input.sessionId ?? "");
27
+ // Determinism: recomputing yields the identical bucket.
28
+ if (a.bucket !== b.bucket) {
29
+ return { ok: false };
30
+ }
31
+ return { ok: true, bucket: a.bucket };
32
+ }
33
+ if (scenario === "gate-power" || scenario === "gate-safety") {
34
+ const ev = fx.input.evidence ?? {};
35
+ const windowStartMs = ev.windowStartMs ?? 0;
36
+ const hardFaults = (ev.hardFaults ?? []).map((f) => ({ kind: f.kind, detail: f.detail }));
37
+ const evidence = {
38
+ windowStartMs,
39
+ powered: ev.powered ?? false,
40
+ events: ev.events ?? 0,
41
+ sessions: ev.sessions ?? 0,
42
+ hardFaults,
43
+ };
44
+ // The spec's unique failure injection: wall-clock jump is irrelevant; only the
45
+ // monotonic elapsed drives eligibility. For the 71h59m row we pass 71h59m; for
46
+ // the wall-jump row we pass the SAME monotonic elapsed (so it must stay blocked);
47
+ // for the >=72h row we pass a full 72h+ monotonic delta (so it advances).
48
+ const start = fx._monotonicElapsedMs;
49
+ const elapsedMs = start ?? GATE_MIN_ELAPSED_MS; // default: a full 72h window
50
+ const clock = clockAt(elapsedMs, windowStartMs);
51
+ // gate-safety with a hard fault forces pre-VC + freezes promotion regardless of
52
+ // the monotonic window; the gate index stays at the input's prior gate (0 here).
53
+ if (scenario === "gate-safety") {
54
+ const outcome = decideGate(0, evidence, clock);
55
+ return {
56
+ ok: true,
57
+ gateIndex: outcome.gateIndex,
58
+ promotionBlocked: outcome.promotionBlocked,
59
+ selectsPreVc: outcome.promotionBlocked,
60
+ };
61
+ }
62
+ const outcome = decideGate(0, evidence, clock);
63
+ return {
64
+ ok: true,
65
+ gateIndex: outcome.gateIndex,
66
+ promotionBlocked: outcome.promotionBlocked,
67
+ };
68
+ }
69
+ throw new Error(`unknown rollout scenario: ${scenario}`);
70
+ }