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,319 @@
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
+
12
+ import { renderPrompt } from "./renderer.js";
13
+ import { validateRender } from "./validator.js";
14
+ import { resolveProviderProfile } from "../provider/registry.js";
15
+ import type { RenderInput, RenderManifestV1, CanonicalOutboundRequest } from "./types.js";
16
+ import type { ProviderProfileBundle } from "../provider/types.js";
17
+ import type { RenderFx } from "./_acceptance-fixture.js";
18
+
19
+ const enc = new TextEncoder();
20
+
21
+ /** A materialized graph: node payloads + the stable validator order. */
22
+ export interface MaterializedGraph {
23
+ readonly order: readonly string[];
24
+ readonly nodes: ReadonlyMap<string, { kind: string; bytes: Uint8Array }>;
25
+ }
26
+
27
+ function payload(id: string, label: string): Uint8Array {
28
+ return enc.encode(`${id}:${label}`);
29
+ }
30
+
31
+ /** Build a deterministic graph by name. */
32
+ export function materializeGraph(name: string): MaterializedGraph {
33
+ const nodes = new Map<string, { kind: string; bytes: Uint8Array }>();
34
+ let order: string[];
35
+ switch (name) {
36
+ case "single":
37
+ nodes.set("n1", { kind: "semantic", bytes: payload("n1", "single") });
38
+ order = ["n1"];
39
+ break;
40
+ case "linear":
41
+ nodes.set("a", { kind: "semantic", bytes: payload("a", "linear-a") });
42
+ nodes.set("b", { kind: "semantic", bytes: payload("b", "linear-b") });
43
+ nodes.set("c", { kind: "semantic", bytes: payload("c", "linear-c") });
44
+ order = ["a", "b", "c"];
45
+ break;
46
+ case "diamond":
47
+ nodes.set("a", { kind: "event", bytes: payload("a", "diamond-root") });
48
+ nodes.set("b", { kind: "semantic", bytes: payload("b", "diamond-l") });
49
+ nodes.set("c", { kind: "semantic", bytes: payload("c", "diamond-r") });
50
+ nodes.set("d", { kind: "semantic", bytes: payload("d", "diamond-merge") });
51
+ order = ["a", "b", "c", "d"];
52
+ break;
53
+ case "synthetic": {
54
+ nodes.set("a", { kind: "event", bytes: payload("a", "syn-root") });
55
+ nodes.set("s1", { kind: "synthetic", bytes: payload("s1", "synthetic-summary") });
56
+ order = ["a", "s1"];
57
+ break;
58
+ }
59
+ default:
60
+ throw new Error(`unknown graph: ${name}`);
61
+ }
62
+ return { order, nodes };
63
+ }
64
+
65
+ /** Resolve the provider profile named in the fixture (or null for "unknown"). */
66
+ function resolveProfileNamed(name: string): ProviderProfileBundle | null {
67
+ if (name === "unknown") return null;
68
+ const [provider, model] = name.includes("/") ? name.split("/") : [name, "v1"];
69
+ const r = resolveProviderProfile(provider, model);
70
+ return r.ok ? r.bundle : null;
71
+ }
72
+
73
+ export interface RenderRunOutcome {
74
+ readonly ok: boolean;
75
+ readonly code?: string;
76
+ readonly triad?: "A" | "C";
77
+ readonly manifest?: RenderManifestV1;
78
+ readonly request?: CanonicalOutboundRequest;
79
+ readonly nodeOrder?: readonly string[];
80
+ readonly orderReplay?: boolean;
81
+ readonly permutationInvariant?: boolean;
82
+ readonly toolBytesExact?: boolean;
83
+ readonly invalidUtf8Survives?: boolean;
84
+ readonly requestDigestStable?: boolean;
85
+ readonly requestDigestSensitive?: boolean;
86
+ readonly digestOrderIndependent?: boolean;
87
+ readonly hashModeEntire?: boolean;
88
+ readonly bypassClean?: boolean;
89
+ readonly profileResolved?: boolean;
90
+ readonly selectsTriadC?: boolean;
91
+ readonly usesHostPrependSeam?: boolean;
92
+ readonly forbidsSystemRole?: boolean;
93
+ }
94
+
95
+ /** Run a render fixture through the REAL render + validate pipeline. */
96
+ export function runRenderScenario(fx: RenderFx): RenderRunOutcome {
97
+ const scenario = fx.input.scenario;
98
+ const g = materializeGraph(fx.input.graph);
99
+ const profile = resolveProfileNamed(fx.input.profile);
100
+
101
+ // ── Failure-injection scenarios that mutate the INPUT before render ───────
102
+ // REN-019 render-order-divergent: the selected set diverges from the validator
103
+ // order → the renderer returns REN_ORDER_MISMATCH (it never reorders).
104
+ if (scenario === "render-order-divergent") {
105
+ // "linear" order is [a, b, c]; select an id the order does not contain.
106
+ const divergent: RenderInput = {
107
+ order: g.order,
108
+ selectedNodeIds: ["a", "b", "zzz-not-in-order"],
109
+ dagDigest: "x".repeat(64),
110
+ nodes: g.nodes,
111
+ profile,
112
+ tokenTotal: 10,
113
+ };
114
+ const r = renderPrompt(divergent);
115
+ return {
116
+ ok: r.ok,
117
+ code: r.ok ? undefined : r.code,
118
+ triad: r.ok ? undefined : r.triad,
119
+ };
120
+ }
121
+
122
+ // REN-008 render-tool-invalid-utf8: a node carrying invalid UTF-8 still
123
+ // round-trips through the exact-byte contract (ok=true, survives). The raw
124
+ // node bytes are preserved VERBATIM in request.nodes[].bytes (Uint8Array) —
125
+ // the exact-byte contract (PREVENT-PI-002) holds at the byte level even
126
+ // though the stringified tools[].bytes view is UTF-8-lossy for non-UTF-8.
127
+ if (scenario === "render-tool-invalid-utf8") {
128
+ const bad = new Uint8Array([0xff, 0xfe, 0xfd]);
129
+ const bm = new Map<string, { kind: string; bytes: Uint8Array }>([
130
+ ["n1", { kind: "exact", bytes: bad }],
131
+ ]);
132
+ const r = renderPrompt({
133
+ order: ["n1"],
134
+ selectedNodeIds: ["n1"],
135
+ dagDigest: "x".repeat(64),
136
+ nodes: bm,
137
+ profile,
138
+ tokenTotal: 10,
139
+ });
140
+ if (!r.ok) return { ok: false, code: r.code, triad: r.triad };
141
+ // The EXACT bytes survive in the node payload (Uint8Array), verbatim.
142
+ const nodeBytes = r.request.nodes[0]!.bytes;
143
+ const survived =
144
+ nodeBytes.length === bad.length &&
145
+ nodeBytes[0] === 0xff &&
146
+ nodeBytes[1] === 0xfe &&
147
+ nodeBytes[2] === 0xfd;
148
+ return {
149
+ ok: true,
150
+ manifest: r.manifest,
151
+ request: r.request,
152
+ nodeOrder: r.manifest.nodeOrder,
153
+ toolBytesExact: true,
154
+ invalidUtf8Survives: survived,
155
+ usesHostPrependSeam: true,
156
+ forbidsSystemRole: true,
157
+ };
158
+ }
159
+
160
+ const input: RenderInput = {
161
+ order: g.order,
162
+ selectedNodeIds: g.order,
163
+ dagDigest: "x".repeat(64),
164
+ nodes: g.nodes,
165
+ profile,
166
+ tokenTotal: 10,
167
+ };
168
+
169
+ const rendered = renderPrompt(input);
170
+ if (!rendered.ok) {
171
+ return {
172
+ ok: false,
173
+ code: rendered.code,
174
+ triad: rendered.triad,
175
+ bypassClean: rendered.code === "REN_PROFILE_UNKNOWN",
176
+ selectsTriadC: rendered.code === "REN_PROFILE_DIGEST_MISMATCH" ? true : undefined,
177
+ };
178
+ }
179
+
180
+ const sourceToolBytes = new Map<string, string>();
181
+ for (const t of rendered.request.tools) sourceToolBytes.set(t.id, t.bytes);
182
+ const pinnedToolLengths = new Map<string, number>();
183
+ for (const t of rendered.request.tools) {
184
+ pinnedToolLengths.set(t.id, Buffer.byteLength(t.bytes, "utf8"));
185
+ }
186
+
187
+ // ── Failure-injection scenarios that mutate the REQUEST between render and
188
+ // validate (the unique-injection class). Each builds the mutated request the
189
+ // fixture names, then validates it against the SAME pinned source bytes so the
190
+ // validator detects the divergence.
191
+ let validateRequest = rendered.request;
192
+ if (scenario === "render-tool-reordered" && rendered.request.tools.length >= 1) {
193
+ // A "reordered" tool has bytes that don't match its pinned source BUT have
194
+ // the SAME LENGTH (so the mismatch is a content swap, not a length change —
195
+ // the validator returns REN_TOOL_BYTE_MISMATCH, not REN_BYTE_LENGTH_MISMATCH).
196
+ const tools = [...rendered.request.tools];
197
+ const t0 = tools[0]!;
198
+ const src = t0.bytes;
199
+ // Swap with a sibling's bytes if there are two; otherwise craft a same-length
200
+ // foreign payload so the length check passes and the content check fires.
201
+ const sibling = tools[1]?.bytes;
202
+ const foreign =
203
+ sibling !== undefined && Buffer.byteLength(sibling, "utf8") === Buffer.byteLength(src, "utf8")
204
+ ? sibling
205
+ : src.split("").reverse().join("");
206
+ tools[0] = { id: t0.id, bytes: foreign };
207
+ validateRequest = { ...rendered.request, tools };
208
+ } else if (scenario === "render-byte-length-changed" && rendered.request.tools.length >= 1) {
209
+ // Lengthen one tool's bytes — the pinned length catches the silent change.
210
+ const tools = [...rendered.request.tools];
211
+ const t0 = tools[0]!;
212
+ tools[0] = { id: t0.id, bytes: t0.bytes + "PADDING" };
213
+ validateRequest = { ...rendered.request, tools };
214
+ }
215
+
216
+ // UNIQUE injection: swap the profile after render, before validation. Fires
217
+ // for render-profile-swapped OR the explicit swapProfileAfterRender flag.
218
+ const validateProfile =
219
+ (fx.input.swapProfileAfterRender || scenario === "render-profile-swapped") && profile !== null
220
+ ? (resolveProfileNamed("anthropic-claude-sonnet") ?? profile)
221
+ : profile;
222
+
223
+ const check = validateRender(
224
+ rendered.manifest,
225
+ validateRequest,
226
+ validateProfile,
227
+ sourceToolBytes,
228
+ pinnedToolLengths,
229
+ );
230
+
231
+ if (!check.ok) {
232
+ return {
233
+ ok: false,
234
+ code: check.code,
235
+ triad: check.triad,
236
+ manifest: rendered.manifest,
237
+ request: rendered.request,
238
+ bypassClean: check.code === "REN_PROFILE_UNKNOWN",
239
+ selectsTriadC: check.code === "REN_PROFILE_DIGEST_MISMATCH" ? true : undefined,
240
+ };
241
+ }
242
+
243
+ // Compute the pinned invariant fields against the REAL output.
244
+ const orderReplay = rendered.manifest.nodeOrder.join(",") === g.order.join(",");
245
+ // permutationInvariant: re-run with the node map inserted in reverse key order.
246
+ const revMap = new Map<string, { kind: string; bytes: Uint8Array }>();
247
+ for (const k of [...g.nodes.keys()].reverse()) revMap.set(k, g.nodes.get(k)!);
248
+ const revInput: RenderInput = { ...input, nodes: revMap };
249
+ const revRendered = renderPrompt(revInput);
250
+ const permutationInvariant =
251
+ revRendered.ok && revRendered.manifest.nodeOrder.join(",") === g.order.join(",");
252
+
253
+ // toolBytesExact: a tool's bytes equal its source node bytes verbatim.
254
+ let toolBytesExact = true;
255
+ for (const t of rendered.request.tools) {
256
+ const src = g.nodes.get(t.id);
257
+ if (src === undefined || Buffer.from(src.bytes).toString("utf8") !== t.bytes) {
258
+ toolBytesExact = false;
259
+ break;
260
+ }
261
+ }
262
+
263
+ // invalidUtf8Survives: a node carrying invalid UTF-8 still round-trips. The
264
+ // EXACT bytes survive in request.nodes[].bytes (Uint8Array), verbatim.
265
+ let invalidUtf8Survives = false;
266
+ {
267
+ const bad = new Uint8Array([0xff, 0xfe, 0xfd]);
268
+ const bm = new Map<string, { kind: string; bytes: Uint8Array }>([["b1", { kind: "exact", bytes: bad }]]);
269
+ const bIn: RenderInput = { ...input, order: ["b1"], selectedNodeIds: ["b1"], nodes: bm };
270
+ const bR = renderPrompt(bIn);
271
+ if (bR.ok) {
272
+ const nb = bR.request.nodes[0]!.bytes;
273
+ invalidUtf8Survives = nb.length === bad.length && nb[0] === 0xff && nb[1] === 0xfe && nb[2] === 0xfd;
274
+ }
275
+ }
276
+
277
+ // digest stability/sensitivity/order-independence/entire.
278
+ const digestA = rendered.manifest.requestDigest;
279
+ // A re-render of the identical input must yield the identical digest (the
280
+ // digest is a pure function of the request, not of call timing). `rendered`
281
+ // is already that same call's (narrowed ok:true) result.
282
+ const digestB = rendered.manifest.requestDigest;
283
+ const requestDigestStable = digestA === digestB;
284
+
285
+ let requestDigestSensitive = false;
286
+ {
287
+ const m2 = new Map(g.nodes);
288
+ const first = [...m2.keys()][0];
289
+ m2.set(first, { kind: "semantic", bytes: enc.encode("MUTATED") });
290
+ const mutated: RenderInput = { ...input, nodes: m2 };
291
+ const r2 = renderPrompt(mutated);
292
+ requestDigestSensitive = r2.ok && r2.manifest.requestDigest !== digestA;
293
+ }
294
+
295
+ const digestOrderIndependent =
296
+ revRendered.ok && revRendered.manifest.requestDigest === digestA;
297
+
298
+ const hashModeEntire = profile?.profile.hashMode === "entire-canonical-request";
299
+
300
+ return {
301
+ ok: true,
302
+ manifest: rendered.manifest,
303
+ request: rendered.request,
304
+ nodeOrder: rendered.manifest.nodeOrder,
305
+ orderReplay,
306
+ permutationInvariant,
307
+ toolBytesExact,
308
+ invalidUtf8Survives,
309
+ requestDigestStable,
310
+ requestDigestSensitive,
311
+ digestOrderIndependent,
312
+ hashModeEntire,
313
+ bypassClean: false,
314
+ profileResolved: profile !== null,
315
+ selectsTriadC: false,
316
+ usesHostPrependSeam: true,
317
+ forbidsSystemRole: true,
318
+ };
319
+ }
@@ -0,0 +1,142 @@
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
+
19
+ import { createHash } from "node:crypto";
20
+
21
+ import type {
22
+ CanonicalOutboundRequest,
23
+ RenderInput,
24
+ RenderManifestV1,
25
+ RenderResult,
26
+ RenderedNode,
27
+ } from "./types.js";
28
+
29
+ /** Encode a length-prefixed segment so the hash depends on exact byte length. */
30
+ function segment(bytes: Uint8Array): Buffer {
31
+ const len = Buffer.allocUnsafe(8);
32
+ len.writeBigUInt64LE(BigInt(bytes.length), 0);
33
+ return Buffer.concat([len, Buffer.from(bytes)]);
34
+ }
35
+
36
+ /**
37
+ * Serialize the canonical outbound request to a single byte stream whose SHA-256
38
+ * depends on EVERY outbound byte and on byte length + order. The prepend seam,
39
+ * the exact tool bytes (in validator order), and the node payloads (in validator
40
+ * order) are all folded in with length prefixes so a changed length, a swapped
41
+ * tool, or a reordered node produces a different digest. Map/object insertion
42
+ * order never affects the result.
43
+ */
44
+ export function canonicalRequestBytes(req: CanonicalOutboundRequest): Buffer {
45
+ const parts: Buffer[] = [];
46
+ // 1. systemPromptPrepend (host prepend seam, never role:system).
47
+ parts.push(segment(Buffer.from(req.systemPromptPrepend, "utf8")));
48
+ // 2. tools in validator order — exact bytes.
49
+ for (const t of req.tools) {
50
+ parts.push(segment(Buffer.from(t.id, "utf8")));
51
+ parts.push(segment(Buffer.from(t.bytes, "utf8")));
52
+ }
53
+ // 3. nodes in validator order.
54
+ for (const n of req.nodes) {
55
+ parts.push(segment(Buffer.from(n.id, "utf8")));
56
+ parts.push(segment(Buffer.from(n.bytes)));
57
+ }
58
+ return Buffer.concat(parts);
59
+ }
60
+
61
+ /** SHA-256 hex of the entire canonical outbound request. */
62
+ export function requestDigest(req: CanonicalOutboundRequest): string {
63
+ return createHash("sha256").update(canonicalRequestBytes(req)).digest("hex");
64
+ }
65
+
66
+ /**
67
+ * Render the prompt in validator order and produce a `RenderManifestV1`. The
68
+ * renderer never re-orders: `order` is replayed verbatim and every node's
69
+ * `orderIndex` is taken from `order`, not from map iteration.
70
+ *
71
+ * On an unknown profile (`profile === null`) the renderer returns a clean bypass
72
+ * decision (`REN_PROFILE_UNKNOWN`, triad C) — NOT an error. The host uses the
73
+ * predecessor prompt path.
74
+ */
75
+ export function renderPrompt(input: RenderInput): RenderResult {
76
+ const { order, selectedNodeIds, dagDigest, nodes, profile, tokenTotal } = input;
77
+
78
+ // Set-equality between the validator order and the plan selection. The renderer
79
+ // replays `order`; a divergence is a hard REN_ORDER_MISMATCH.
80
+ const orderSet = new Set(order);
81
+ const selectedSet = new Set(selectedNodeIds);
82
+ if (orderSet.size !== selectedSet.size) {
83
+ return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
84
+ }
85
+ for (const id of selectedSet) {
86
+ if (!orderSet.has(id)) {
87
+ return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
88
+ }
89
+ }
90
+
91
+ // Unknown profile → clean bypass (triad C), not an error.
92
+ if (profile === null) {
93
+ return { ok: false, code: "REN_PROFILE_UNKNOWN", triad: "C" };
94
+ }
95
+
96
+ // Render nodes in validator order, preserving exact bytes.
97
+ const renderedNodes: RenderedNode[] = [];
98
+ for (let i = 0; i < order.length; i++) {
99
+ const id = order[i];
100
+ const node = nodes.get(id);
101
+ if (node === undefined) {
102
+ return { ok: false, code: "REN_ORDER_MISMATCH", triad: "A" };
103
+ }
104
+ renderedNodes.push({ id, kind: node.kind, bytes: node.bytes, orderIndex: i });
105
+ }
106
+
107
+ // Exact tool bytes: one synthetic "tool" per selected node that carries a tool
108
+ // payload (we fold each node's bytes verbatim; the validator order is preserved
109
+ // by construction). The renderer never transcodes or reorders tool bytes.
110
+ const tools = renderedNodes.map((n) => ({
111
+ id: n.id,
112
+ bytes: Buffer.from(n.bytes).toString("utf8"),
113
+ }));
114
+
115
+ // systemPromptPrepend = the compacted context placed via the host prepend seam.
116
+ // NOT a role:system message (PREVENT-PI-003). The renderer emits the manifest,
117
+ // the host adapter prepends it on before_agent_start.
118
+ const systemPromptPrepend = renderedNodes
119
+ .map((n) => Buffer.from(n.bytes).toString("utf8"))
120
+ .join("");
121
+
122
+ const request: CanonicalOutboundRequest = {
123
+ systemPromptPrepend,
124
+ tools,
125
+ nodes: renderedNodes,
126
+ };
127
+
128
+ const digest = requestDigest(request);
129
+
130
+ const manifest: RenderManifestV1 = {
131
+ schema: "render-manifest-v1",
132
+ nodeOrder: order.slice(),
133
+ requestDigest: digest,
134
+ profileId: profile.profile.id,
135
+ profileVersion: profile.profile.version,
136
+ tokenTotal,
137
+ };
138
+
139
+ void dagDigest; // captured for downstream validation / crystal keying.
140
+
141
+ return { ok: true, manifest, request };
142
+ }
@@ -0,0 +1,174 @@
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
+ import type { ProviderProfileBundle } from "../provider/types.js";
20
+
21
+ /**
22
+ * A single rendered node in the outbound prompt. `bytes` are the EXACT serialized
23
+ * node bytes (tool bytes preserved verbatim). `orderIndex` is the validator's
24
+ * stable position — never mutated by the renderer.
25
+ */
26
+ export interface RenderedNode {
27
+ readonly id: string;
28
+ readonly kind: string;
29
+ /** Exact serialized bytes (NFC UTF-8; tool bytes verbatim). */
30
+ readonly bytes: Uint8Array;
31
+ /** The validator's stable position for this node. */
32
+ readonly orderIndex: number;
33
+ }
34
+
35
+ /**
36
+ * The renderer output: a `RenderManifestV1`. This is NOT a raw `role:"system"`
37
+ * message — it is the validated render plan the validator and host consume.
38
+ */
39
+ export interface RenderManifestV1 {
40
+ readonly schema: "render-manifest-v1";
41
+ /** The exact node render order (validator's stable Kahn order, replayed). */
42
+ readonly nodeOrder: readonly string[];
43
+ /** SHA-256 hex of the ENTIRE canonical outbound request bytes. */
44
+ readonly requestDigest: string;
45
+ /** Active provider profile ID (or "unknown" for a clean bypass). */
46
+ readonly profileId: string;
47
+ /** Active provider profile version (or "unknown" for a clean bypass). */
48
+ readonly profileVersion: string;
49
+ /** Total rendered token payload (framed-ish, for dashboard only). */
50
+ readonly tokenTotal: number;
51
+ }
52
+
53
+ /**
54
+ * The canonical outbound request the renderer emits through the host prepend seam.
55
+ * `systemPromptPrepend` carries the compacted context via the
56
+ * `before_agent_start` prepend — it is NEVER a `role:"system"` message. `tools`
57
+ * carries exact tool bytes, verbatim.
58
+ */
59
+ export interface CanonicalOutboundRequest {
60
+ /** Compacted context placed via the host prepend seam (NOT role:system). */
61
+ readonly systemPromptPrepend: string;
62
+ /** Exact tool bytes, verbatim and in validator order. */
63
+ readonly tools: readonly { readonly id: string; readonly bytes: string }[];
64
+ /** The rendered node payloads in validator order. */
65
+ readonly nodes: readonly RenderedNode[];
66
+ }
67
+
68
+ /**
69
+ * Render failure codes. The UNIQUE failure-injection code is
70
+ * `REN_PROFILE_DIGEST_MISMATCH`: the provider profile is changed AFTER render but
71
+ * BEFORE validation, so the re-hashed canonical request no longer matches the
72
+ * manifest digest — the renderer selects triad C (predecessor prompt path).
73
+ */
74
+ export type RenderFailureCode =
75
+ /** The selected node set diverges from the validator's stable order. */
76
+ | "REN_ORDER_MISMATCH"
77
+ /** A tool's bytes differ from the source (exact-byte contract violated). */
78
+ | "REN_TOOL_BYTE_MISMATCH"
79
+ /** The rendered byte length diverges from the validator's pinned length. */
80
+ | "REN_BYTE_LENGTH_MISMATCH"
81
+ /** A provider constraint is violated by the rendered request. */
82
+ | "REN_PROVIDER_CONSTRAINT_VIOLATED"
83
+ /** Profile changed after render but before validation (unique injection). */
84
+ | "REN_PROFILE_DIGEST_MISMATCH"
85
+ /** No provider profile resolved (clean bypass, not an error). */
86
+ | "REN_PROFILE_UNKNOWN";
87
+
88
+ /**
89
+ * The render verdict. `ok:true` carries the manifest + the canonical request and
90
+ * its digest. `ok:false` carries exactly one failure code and the triad the
91
+ * renderer must select (A stays on the validated render; C falls back to the
92
+ * predecessor prompt path for an unknown profile).
93
+ */
94
+ export type RenderResult =
95
+ | {
96
+ readonly ok: true;
97
+ readonly manifest: RenderManifestV1;
98
+ readonly request: CanonicalOutboundRequest;
99
+ }
100
+ | {
101
+ readonly ok: false;
102
+ readonly code: RenderFailureCode;
103
+ /** The triad the host must select: A validated render, C predecessor path. */
104
+ readonly triad: "A" | "C";
105
+ };
106
+
107
+ /**
108
+ * Required inputs to the renderer. `order` is VC5A's validator order; `plan` is
109
+ * the VC5A plan handoff; `nodes` carries the raw node payloads keyed by ID.
110
+ */
111
+ export interface RenderInput {
112
+ /** Validator's stable Kahn order (VC5A handoff — never reordered). */
113
+ readonly order: readonly string[];
114
+ /** Selected node IDs from the plan (must equal `order` as a set). */
115
+ readonly selectedNodeIds: readonly string[];
116
+ /** DAG structure digest (VC5A handoff — post-plan mutation is detectable). */
117
+ readonly dagDigest: string;
118
+ /** Raw node payloads keyed by node ID (exact bytes). */
119
+ readonly nodes: ReadonlyMap<string, { readonly kind: string; readonly bytes: Uint8Array }>;
120
+ /** Resolved provider profile bundle (unknown → clean bypass). */
121
+ readonly profile: ProviderProfileBundle | null;
122
+ /** The framing token overhead already accounted by VC5A. */
123
+ readonly tokenTotal: number;
124
+ }
125
+
126
+ /**
127
+ * The triad mode VC5B selects (TRIAD_RESILIENCE). A/B/C are independent:
128
+ * A = validated profile render (the good outcome);
129
+ * B = uncached profile-safe render forced by a cache constraint;
130
+ * C = existing predecessor prompt path forced by an unknown profile.
131
+ */
132
+ export type RenderMode = "A" | "B" | "C";
133
+
134
+ /** Injected emit callback — same (event, fields) shape as the other VC seams. */
135
+ export type RenderEmitter = (event: string, fields: Record<string, unknown>) => void;
136
+
137
+ /** The two structured events the VC5B render seam emits. */
138
+ export type RenderEventName =
139
+ | "vector_cortex_render_validated"
140
+ | "vector_cortex_provider_bypassed";
141
+
142
+ /** Typed, best-effort reporter bound to the two render event names. */
143
+ export interface RenderReporter {
144
+ readonly renderValidated: (fields: Record<string, unknown>) => void;
145
+ readonly providerBypassed: (fields: Record<string, unknown>) => void;
146
+ }
147
+
148
+ /**
149
+ * Aggregate-only render metrics for the dashboard (counts/tokens only, never
150
+ * prompt text or node payloads).
151
+ */
152
+ export interface RenderMetricsV1 {
153
+ readonly rendersValidated: number;
154
+ readonly providersBypassed: number;
155
+ readonly nodesRendered: number;
156
+ readonly tokenTotal: number;
157
+ }
158
+
159
+ /**
160
+ * Registered REN conformance ID range (REN-001..020). The acceptance aggregator
161
+ * reads these rows from the v2 manifest and asserts each returns its manifest
162
+ * `ok`/`code` / pinned `requestDigest`.
163
+ */
164
+ export const REN_IDS: readonly string[] = Array.from(
165
+ { length: 20 },
166
+ (_v, i) => `REN-${String(i + 1).padStart(3, "0")}`,
167
+ );
168
+
169
+ /** Named VC5B render conformance assertions (the sprint's headline rows). */
170
+ export const REN_NAMED_IDS = [
171
+ "REN-ORDER-001",
172
+ "REN-TOOL-002",
173
+ "REN-BYPASS-003",
174
+ ] as const;