pi-mega-compact 0.20.4 → 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 +3 -1
- 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 +2 -0
- 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,163 @@
|
|
|
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
|
+
/**
|
|
20
|
+
* The hash mode the renderer uses for the canonical outbound request. The ONLY
|
|
21
|
+
* supported mode is hashing the entire request bytes — partial hashing is not
|
|
22
|
+
* allowed (it would let a folded field silently break cache identity).
|
|
23
|
+
*/
|
|
24
|
+
export type ProviderHashMode = "entire-canonical-request";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A JSON-pointer exclusion: a documented proof that the named pointer cannot
|
|
28
|
+
* affect provider cache identity. `fixtureId` pins the conformance row that
|
|
29
|
+
* proves it; `proofDigest` is that fixture's SHA-256. Exclusions are versioned
|
|
30
|
+
* per profile and MUST be listed in the manifest (CONTRACTS §ProviderProfileV1).
|
|
31
|
+
*/
|
|
32
|
+
export interface ProviderProfileExclusion {
|
|
33
|
+
readonly pointer: string;
|
|
34
|
+
readonly fixtureId: string;
|
|
35
|
+
readonly proofDigest: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The provider-profile contract. A renderer consumes a profile to decide how the
|
|
40
|
+
* rendered prompt is serialized into the canonical outbound request without
|
|
41
|
+
* invalidating the provider's cache identity.
|
|
42
|
+
*/
|
|
43
|
+
export interface ProviderProfileV1 {
|
|
44
|
+
readonly schema: "provider-profile-v1";
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly version: string;
|
|
47
|
+
readonly hashMode: ProviderHashMode;
|
|
48
|
+
/** Versioned, fixture-proven exclusions (empty = hash the whole request). */
|
|
49
|
+
readonly excludedJsonPointers: readonly ProviderProfileExclusion[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Role rules: how the rendered context may be placed relative to the host system
|
|
54
|
+
* prompt. The renderer MUST use the host `before_agent_start` systemPrompt
|
|
55
|
+
* prepend seam — it NEVER injects compacted context as `role:"system"`
|
|
56
|
+
* (PREVENT-PI-003, critical).
|
|
57
|
+
*/
|
|
58
|
+
export interface ProviderRoleRule {
|
|
59
|
+
/** Always true for VC5B: placement is the host prepend seam, never role:system. */
|
|
60
|
+
readonly useHostPrependSeam: true;
|
|
61
|
+
/** A renderer MUST NOT emit a `role:"system"` message for compacted context. */
|
|
62
|
+
readonly forbidSystemRoleInjection: true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Tool rules: the renderer preserves EXACT tool bytes (PREVENT-PI-002 — never
|
|
67
|
+
* split a toolCall/toolResult pair). A profile declares whether the provider
|
|
68
|
+
* tolerates tool reordering; VC5B renders in the validator's stable order and
|
|
69
|
+
* never reorders, so this is a validation constraint, not a permission.
|
|
70
|
+
*/
|
|
71
|
+
export interface ProviderToolRule {
|
|
72
|
+
/** Exact tool bytes are preserved verbatim (no normalization/transcoding). */
|
|
73
|
+
readonly preserveExactToolBytes: true;
|
|
74
|
+
/** The renderer never reorders tools away from the validator's order. */
|
|
75
|
+
readonly forbidToolReorder: true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Cache rules: what the profile pins about provider cache identity so the
|
|
80
|
+
* renderer's canonical hash stays stable across otherwise-equivalent renders.
|
|
81
|
+
*/
|
|
82
|
+
export interface ProviderCacheRule {
|
|
83
|
+
/** The hash mode the renderer uses when this profile is active. */
|
|
84
|
+
readonly hashMode: ProviderHashMode;
|
|
85
|
+
/** Cache-stable fields the renderer must not reorder or drop. */
|
|
86
|
+
readonly stableFields: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A fully-resolved provider profile bundle: the contract plus the role/tool/cache
|
|
91
|
+
* rules the renderer enforces. The fixture-backed base registry returns these.
|
|
92
|
+
*/
|
|
93
|
+
export interface ProviderProfileBundle {
|
|
94
|
+
readonly profile: ProviderProfileV1;
|
|
95
|
+
readonly role: ProviderRoleRule;
|
|
96
|
+
readonly tool: ProviderToolRule;
|
|
97
|
+
readonly cache: ProviderCacheRule;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Provider profile resolution failure codes. These are NOT errors that should
|
|
102
|
+
* break the agent loop — `PRO_PROFILE_UNKNOWN` is a clean bypass signal.
|
|
103
|
+
*/
|
|
104
|
+
export type ProviderProfileCode =
|
|
105
|
+
/** No matching profile for the provider/model; the renderer bypasses cleanly. */
|
|
106
|
+
| "PRO_PROFILE_UNKNOWN"
|
|
107
|
+
/** A profile matched but its version is unsupported by the renderer. */
|
|
108
|
+
| "PRO_PROFILE_VERSION_UNSUPPORTED";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The provider-profile resolution verdict. `ok:true` carries the resolved
|
|
112
|
+
* bundle; `ok:false` carries a bypass code. Every `ok:false` is a CLEAN bypass
|
|
113
|
+
* (the host uses the predecessor prompt path) — never an error.
|
|
114
|
+
*/
|
|
115
|
+
export type ProviderProfileResult =
|
|
116
|
+
| { readonly ok: true; readonly bundle: ProviderProfileBundle }
|
|
117
|
+
| { readonly ok: false; readonly code: ProviderProfileCode };
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The sentinel returned for an unknown provider/model. The renderer sees this as
|
|
121
|
+
* a CLEAN bypass decision, not an error, and emits `vector_cortex_provider_bypassed`.
|
|
122
|
+
*/
|
|
123
|
+
export const PRO_PROFILE_UNKNOWN: ProviderProfileCode = "PRO_PROFILE_UNKNOWN";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Registered PRO conformance ID range (PRO-001..015). The acceptance aggregator
|
|
127
|
+
* reads these rows from the v2 manifest and asserts each returns its manifest
|
|
128
|
+
* `ok`/`code` / pinned `requestDigest`.
|
|
129
|
+
*/
|
|
130
|
+
export const PRO_IDS: readonly string[] = Array.from(
|
|
131
|
+
{ length: 15 },
|
|
132
|
+
(_v, i) => `PRO-${String(i + 1).padStart(3, "0")}`,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
/** Named VC5B provider conformance assertions (the sprint's headline rows). */
|
|
136
|
+
export const PRO_NAMED_IDS = ["PRO-UNKNOWN-003"] as const;
|
|
137
|
+
|
|
138
|
+
/** Injected emit callback — same (event, fields) shape as the other VC seams. */
|
|
139
|
+
export type ProviderEmitter = (
|
|
140
|
+
event: string,
|
|
141
|
+
fields: Record<string, unknown>,
|
|
142
|
+
) => void;
|
|
143
|
+
|
|
144
|
+
/** The two structured events the VC5B provider seam emits. */
|
|
145
|
+
export type ProviderEventName =
|
|
146
|
+
| "vector_cortex_render_validated"
|
|
147
|
+
| "vector_cortex_provider_bypassed";
|
|
148
|
+
|
|
149
|
+
/** Typed, best-effort reporter bound to the two provider/render event names. */
|
|
150
|
+
export interface ProviderReporter {
|
|
151
|
+
readonly renderValidated: (fields: Record<string, unknown>) => void;
|
|
152
|
+
readonly providerBypassed: (fields: Record<string, unknown>) => void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Aggregate-only provider/render metrics for the dashboard (counts only, never
|
|
157
|
+
* prompt text or node payloads).
|
|
158
|
+
*/
|
|
159
|
+
export interface ProviderMetricsV1 {
|
|
160
|
+
readonly rendersValidated: number;
|
|
161
|
+
readonly providersBypassed: number;
|
|
162
|
+
readonly nodesRendered: number;
|
|
163
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render/_acceptance-fixture.ts — conformance fixture I/O for VC5B render +
|
|
3
|
+
* provider acceptance rows.
|
|
4
|
+
*
|
|
5
|
+
* Reads the v2 conformance manifest + the per-sprint fixture files, and exposes
|
|
6
|
+
* the `RenderFx` / `ProviderFx` shapes the acceptance aggregator drives. No
|
|
7
|
+
* mocks/stubs: the fixtures are the committed canonical corpus.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
10
|
+
import { join, dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
function repoRoot(from: string): string {
|
|
17
|
+
let dir = from;
|
|
18
|
+
for (let i = 0; i < 8; i++) {
|
|
19
|
+
if (existsSync(join(dir, "conformance", "vector-cortex"))) return dir;
|
|
20
|
+
const next = dirname(dir);
|
|
21
|
+
if (next === dir) break;
|
|
22
|
+
dir = next;
|
|
23
|
+
}
|
|
24
|
+
throw new Error("conformance corpus not found above " + from);
|
|
25
|
+
}
|
|
26
|
+
export const REPO_ROOT = repoRoot(HERE);
|
|
27
|
+
export const V2 = join(REPO_ROOT, "conformance", "vector-cortex", "v2");
|
|
28
|
+
|
|
29
|
+
export interface ManifestRow {
|
|
30
|
+
id: string;
|
|
31
|
+
path: string;
|
|
32
|
+
algorithm: string;
|
|
33
|
+
expected: string;
|
|
34
|
+
}
|
|
35
|
+
export interface Manifest {
|
|
36
|
+
fixtures: ManifestRow[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readManifest(): Manifest {
|
|
40
|
+
return JSON.parse(readFileSync(join(V2, "manifest.json"), "utf8")) as Manifest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readFixture(id: string, prefix: "render" | "provider"): unknown {
|
|
44
|
+
const m = readManifest();
|
|
45
|
+
const row = m.fixtures.find((f) => f.id === id && f.path.startsWith(`${prefix}/`));
|
|
46
|
+
assert.ok(row, `fixture ${id} registered under ${prefix}/ in manifest`);
|
|
47
|
+
return JSON.parse(readFileSync(join(V2, row.path), "utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RenderFxInput {
|
|
51
|
+
scenario: string;
|
|
52
|
+
graph: string;
|
|
53
|
+
profile: string;
|
|
54
|
+
mutateByteAfterRender?: boolean;
|
|
55
|
+
swapProfileAfterRender?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface RenderFxExpected {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
code?: string;
|
|
60
|
+
nodeOrder?: string[];
|
|
61
|
+
orderReplay?: boolean;
|
|
62
|
+
permutationInvariant?: boolean;
|
|
63
|
+
toolBytesExact?: boolean;
|
|
64
|
+
invalidUtf8Survives?: boolean;
|
|
65
|
+
requestDigestStable?: boolean;
|
|
66
|
+
requestDigestSensitive?: boolean;
|
|
67
|
+
digestOrderIndependent?: boolean;
|
|
68
|
+
hashModeEntire?: boolean;
|
|
69
|
+
bypassClean?: boolean;
|
|
70
|
+
profileResolved?: boolean;
|
|
71
|
+
selectsTriadC?: boolean;
|
|
72
|
+
usesHostPrependSeam?: boolean;
|
|
73
|
+
forbidsSystemRole?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface RenderFx {
|
|
76
|
+
id: string;
|
|
77
|
+
schema: string;
|
|
78
|
+
producer: string;
|
|
79
|
+
assertion: string;
|
|
80
|
+
kind: string;
|
|
81
|
+
input: RenderFxInput;
|
|
82
|
+
expected: RenderFxExpected;
|
|
83
|
+
}
|
|
84
|
+
export function renderFixture(id: string): RenderFx {
|
|
85
|
+
return readFixture(id, "render") as RenderFx;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ProviderFxInput {
|
|
89
|
+
scenario: string;
|
|
90
|
+
provider: string;
|
|
91
|
+
model: string;
|
|
92
|
+
}
|
|
93
|
+
export interface ProviderFxExpected {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
code?: string;
|
|
96
|
+
profileId?: string;
|
|
97
|
+
hashMode?: string;
|
|
98
|
+
excludedPointers?: string[];
|
|
99
|
+
bypassClean?: boolean;
|
|
100
|
+
cacheStable?: boolean;
|
|
101
|
+
deterministic?: boolean;
|
|
102
|
+
}
|
|
103
|
+
export interface ProviderFx {
|
|
104
|
+
id: string;
|
|
105
|
+
schema: string;
|
|
106
|
+
producer: string;
|
|
107
|
+
assertion: string;
|
|
108
|
+
kind: string;
|
|
109
|
+
input: ProviderFxInput;
|
|
110
|
+
expected: ProviderFxExpected;
|
|
111
|
+
}
|
|
112
|
+
export function providerFixture(id: string): ProviderFx {
|
|
113
|
+
return readFixture(id, "provider") as ProviderFx;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Flag-pinned wrapper: VC5B gated by MEGACOMPACT_VC5B (defaults ON). */
|
|
117
|
+
export function withFlagsOn(fn: () => void): () => void {
|
|
118
|
+
return (): void => {
|
|
119
|
+
const saved = process.env.MEGACOMPACT_VC5B;
|
|
120
|
+
process.env.MEGACOMPACT_VC5B = "1";
|
|
121
|
+
try {
|
|
122
|
+
fn();
|
|
123
|
+
} finally {
|
|
124
|
+
if (saved === undefined) delete process.env.MEGACOMPACT_VC5B;
|
|
125
|
+
else process.env.MEGACOMPACT_VC5B = saved;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render/_acceptance-helpers.ts — barrel re-export for the VC5B render + provider
|
|
3
|
+
* acceptance rows.
|
|
4
|
+
*
|
|
5
|
+
* The implementation is split across focused sibling files to keep each under
|
|
6
|
+
* the src/ 500-line hard limit; this file is the single import surface the
|
|
7
|
+
* acceptance aggregator uses:
|
|
8
|
+
* - `_acceptance-fixture.ts` — manifest I/O + RenderFx/ProviderFx shapes + withFlagsOn
|
|
9
|
+
* - `_acceptance-scenario.ts` — declarative graph materialization + runRenderScenario
|
|
10
|
+
* - `_acceptance-provider.ts` — provider-registry runner + KNOWN_KEYS
|
|
11
|
+
*
|
|
12
|
+
* Every function drives the REAL render / validate / provider logic — no mocks,
|
|
13
|
+
* no stubs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
REPO_ROOT,
|
|
18
|
+
V2,
|
|
19
|
+
readManifest,
|
|
20
|
+
renderFixture,
|
|
21
|
+
providerFixture,
|
|
22
|
+
withFlagsOn,
|
|
23
|
+
} from "./_acceptance-fixture.js";
|
|
24
|
+
export type {
|
|
25
|
+
ManifestRow,
|
|
26
|
+
Manifest,
|
|
27
|
+
RenderFxInput,
|
|
28
|
+
RenderFxExpected,
|
|
29
|
+
RenderFx,
|
|
30
|
+
ProviderFxInput,
|
|
31
|
+
ProviderFxExpected,
|
|
32
|
+
ProviderFx,
|
|
33
|
+
} from "./_acceptance-fixture.js";
|
|
34
|
+
|
|
35
|
+
export { materializeGraph, runRenderScenario } from "./_acceptance-scenario.js";
|
|
36
|
+
export type { MaterializedGraph, RenderRunOutcome } from "./_acceptance-scenario.js";
|
|
37
|
+
|
|
38
|
+
export { runProviderScenario, KNOWN_KEYS } from "./_acceptance-provider.js";
|
|
39
|
+
export type { ProviderRunOutcome } from "./_acceptance-provider.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render/_acceptance-provider.ts — the REAL provider-registry runner for VC5B
|
|
3
|
+
* provider acceptance rows. Drives `resolveProviderProfile` (no mocks).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolveProviderProfile, KNOWN_PROVIDER_KEYS } from "../provider/registry.js";
|
|
7
|
+
import type { ProviderFx } from "./_acceptance-fixture.js";
|
|
8
|
+
|
|
9
|
+
export interface ProviderRunOutcome {
|
|
10
|
+
readonly ok: boolean;
|
|
11
|
+
readonly code?: string;
|
|
12
|
+
readonly profileId?: string;
|
|
13
|
+
readonly hashMode?: string;
|
|
14
|
+
readonly excludedPointers?: string[];
|
|
15
|
+
readonly bypassClean?: boolean;
|
|
16
|
+
readonly cacheStable?: boolean;
|
|
17
|
+
readonly deterministic?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Run a provider fixture through the REAL registry resolver. */
|
|
21
|
+
export function runProviderScenario(fx: ProviderFx): ProviderRunOutcome {
|
|
22
|
+
const r = resolveProviderProfile(fx.input.provider, fx.input.model);
|
|
23
|
+
if (!r.ok) {
|
|
24
|
+
return { ok: false, code: r.code, bypassClean: true };
|
|
25
|
+
}
|
|
26
|
+
const p = r.bundle.profile;
|
|
27
|
+
const cacheStable =
|
|
28
|
+
fx.input.scenario === "resolve-excluded-cache" &&
|
|
29
|
+
p.excludedJsonPointers.length === 1 &&
|
|
30
|
+
p.excludedJsonPointers[0].pointer === "/requestId";
|
|
31
|
+
// deterministic: a second identical lookup returns the same profile id.
|
|
32
|
+
const r2 = resolveProviderProfile(fx.input.provider, fx.input.model);
|
|
33
|
+
const deterministic = r2.ok && r2.bundle.profile.id === p.id;
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
profileId: p.id,
|
|
37
|
+
hashMode: p.hashMode,
|
|
38
|
+
excludedPointers: p.excludedJsonPointers.map((e) => e.pointer),
|
|
39
|
+
cacheStable,
|
|
40
|
+
deterministic,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Exposed for the acceptance registration check. */
|
|
45
|
+
export const KNOWN_KEYS = KNOWN_PROVIDER_KEYS;
|
|
@@ -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
|
+
}
|