pi-mega-compact 0.20.2 → 0.20.4
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 +13 -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-plans.js +49 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +29 -22
- package/dist/src/config/vector-cortex.js +13 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/planner/manifest.js +66 -0
- package/dist/src/vector-cortex/planner/portfolio.js +0 -0
- package/dist/src/vector-cortex/planner/types.js +41 -0
- package/dist/src/vector-cortex/prompt-dag/_acceptance-dag.js +347 -0
- package/dist/src/vector-cortex/prompt-dag/_acceptance-fixture.js +59 -0
- package/dist/src/vector-cortex/prompt-dag/_acceptance-helpers.js +17 -0
- package/dist/src/vector-cortex/prompt-dag/_acceptance-planner.js +153 -0
- package/dist/src/vector-cortex/prompt-dag/_acceptance-shuffle.js +24 -0
- package/dist/src/vector-cortex/prompt-dag/builder.js +171 -0
- package/dist/src/vector-cortex/prompt-dag/types.js +33 -0
- package/dist/src/vector-cortex/prompt-dag/validator.js +0 -0
- package/dist/vector-cortex/planner/manifest.js +66 -0
- package/dist/vector-cortex/planner/portfolio.js +0 -0
- package/dist/vector-cortex/planner/types.js +41 -0
- package/dist/vector-cortex/prompt-dag/_acceptance-dag.js +347 -0
- package/dist/vector-cortex/prompt-dag/_acceptance-fixture.js +59 -0
- package/dist/vector-cortex/prompt-dag/_acceptance-helpers.js +17 -0
- package/dist/vector-cortex/prompt-dag/_acceptance-planner.js +153 -0
- package/dist/vector-cortex/prompt-dag/_acceptance-shuffle.js +24 -0
- package/dist/vector-cortex/prompt-dag/builder.js +171 -0
- package/dist/vector-cortex/prompt-dag/types.js +33 -0
- package/dist/vector-cortex/prompt-dag/validator.js +0 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-XF3jPpvn.js → AreaChart-Cam9-nhT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-XF3jPpvn.js.map → AreaChart-Cam9-nhT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-DnYYn0GF.js → BarChart-DzIY8aM1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-DnYYn0GF.js.map → BarChart-DzIY8aM1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-D3bVskCL.js → CacheTab-CKIiYsG_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-D3bVskCL.js.map → CacheTab-CKIiYsG_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CX_8myfc.js → EventsTab-xaaBJLiK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CX_8myfc.js.map → EventsTab-xaaBJLiK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-CnVzBQvN.js → HealthTab-BGtUKOqG.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-CnVzBQvN.js.map → HealthTab-BGtUKOqG.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cao--dtV.js → MaintenanceTab-ABgQnTZ9.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Cao--dtV.js.map → MaintenanceTab-ABgQnTZ9.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DZdI-9ig.js → MemoryMapTab-ChR2GAN-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DZdI-9ig.js.map → MemoryMapTab-ChR2GAN-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-SlpSmNU4.js → MetricsTab-DdCLAgcR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-SlpSmNU4.js.map → MetricsTab-DdCLAgcR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-C_-_4au9.js → OverviewTab-DtemzJt6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-C_-_4au9.js.map → OverviewTab-DtemzJt6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-CyWedWZC.js → ReposTab-B9mvD9Ep.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-CyWedWZC.js.map → ReposTab-B9mvD9Ep.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-wqPBMIJZ.js → SessionsTab-BcO38XLS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-wqPBMIJZ.js.map → SessionsTab-BcO38XLS.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-MojXmuUf.js → SetupTab-D_TBNP09.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-MojXmuUf.js.map → SetupTab-D_TBNP09.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BxcplEOJ.js → TimeSavedCard-TjHpbUOo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BxcplEOJ.js.map → TimeSavedCard-TjHpbUOo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-D8_qWbjY.js → TurnsTab-DprxndCF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-D8_qWbjY.js.map → TurnsTab-DprxndCF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Bym21yVQ.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Bym21yVQ.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-Cfd-QZYz.js → WikiTab-OKgSo15G.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-Cfd-QZYz.js.map → WikiTab-OKgSo15G.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-tX5ht0bb.js → button-D_Qz4q7L.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-tX5ht0bb.js.map → button-D_Qz4q7L.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-BT3Z5dDg.js → card-CsI6Jm3p.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-BT3Z5dDg.js.map → card-CsI6Jm3p.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B6p2WnkY.js → generateCategoricalChart-CvHKUdhX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-B6p2WnkY.js.map → generateCategoricalChart-CvHKUdhX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-CFLekP12.js → index-0Ye_un22.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-CFLekP12.js.map → index-0Ye_un22.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-B52oAMSw.js → switch-VQEoJl8Q.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-B52oAMSw.js.map → switch-VQEoJl8Q.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-VxxVc5Tp.js → toggle-CAPm4HJo.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-VxxVc5Tp.js.map → toggle-CAPm4HJo.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-DAVdZnG7.js → useSSE-pxsNTVkh.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-DAVdZnG7.js.map → useSSE-pxsNTVkh.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/VectorCortexTab.tsx +35 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +20 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +38 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-plans.ts +58 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +1 -0
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +31 -22
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +14 -0
- package/src/config.ts +1 -0
- package/src/vector-cortex/planner/manifest.ts +83 -0
- package/src/vector-cortex/planner/portfolio.ts +0 -0
- package/src/vector-cortex/planner/types.ts +190 -0
- package/src/vector-cortex/prompt-dag/_acceptance-dag.ts +392 -0
- package/src/vector-cortex/prompt-dag/_acceptance-fixture.ts +132 -0
- package/src/vector-cortex/prompt-dag/_acceptance-helpers.ts +39 -0
- package/src/vector-cortex/prompt-dag/_acceptance-planner.ts +188 -0
- package/src/vector-cortex/prompt-dag/_acceptance-shuffle.ts +26 -0
- package/src/vector-cortex/prompt-dag/builder.ts +189 -0
- package/src/vector-cortex/prompt-dag/types.ts +151 -0
- package/src/vector-cortex/prompt-dag/validator.ts +0 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-B3yisAyB.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-B3yisAyB.js.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/prompt-dag/builder.ts — single-session PromptDagV1 construction
|
|
3
|
+
* (VC5A, task 2).
|
|
4
|
+
*
|
|
5
|
+
* The builder is the STRUCTURAL gate: it rejects a graph that could never be
|
|
6
|
+
* planned safely, before any ordering or selection is attempted. Rejections are
|
|
7
|
+
* hard and total (the sprint bar: "cycles/incompatibilities rejected 100%"):
|
|
8
|
+
*
|
|
9
|
+
* - `DAG_MIXED_SESSION` — a span whose sessionId differs from the DAG's.
|
|
10
|
+
* Cross-session evidence MUST be a synthetic
|
|
11
|
+
* node citing a separately validated manifest,
|
|
12
|
+
* never a foreign span (CONTRACTS §PromptDagV1).
|
|
13
|
+
* - `DAG_DUPLICATE_ID` — two nodes sharing an id.
|
|
14
|
+
* - `DAG_MISSING_ENDPOINT` — an edge naming an absent node.
|
|
15
|
+
* - `DAG_INVALID_SPAN` — reversed/negative range, or a synthetic node
|
|
16
|
+
* carrying a span (synthetics have no source
|
|
17
|
+
* position by definition).
|
|
18
|
+
* - `DAG_SPAN_DIGEST_CONFLICT` — two overlapping spans pinning DIFFERENT
|
|
19
|
+
* digests for the same bytes: a corrupted or
|
|
20
|
+
* forged source claim, not a legal overlap.
|
|
21
|
+
* - `DAG_UNKNOWN_INCOMPATIBLE` — `incompatibleWith` naming an absent node.
|
|
22
|
+
* - `DAG_TOOL_PAIR_SPLIT` — a tool-pair edge whose endpoints are not both
|
|
23
|
+
* present (PREVENT-PI-002).
|
|
24
|
+
*
|
|
25
|
+
* Cycle and reversed-`precedes` detection belong to the VALIDATOR, which owns
|
|
26
|
+
* the topological order; the builder only guarantees the graph is structurally
|
|
27
|
+
* well-formed and single-session.
|
|
28
|
+
*
|
|
29
|
+
* Determinism: nodes and edges are emitted in a canonical sorted order so the
|
|
30
|
+
* same inputs always produce byte-identical DAG bytes, whatever order the caller
|
|
31
|
+
* supplied them in. Pure: no storage, no console, no network (PREVENT-PI-004).
|
|
32
|
+
*/
|
|
33
|
+
/** Bytewise string comparison — the universal deterministic tie-break. */
|
|
34
|
+
function byBytes(a, b) {
|
|
35
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Canonical node order: source position first (`startSeq`, then `startByte`),
|
|
39
|
+
* synthetics last (they have no span), then `syntheticOrdinal`, then id bytes.
|
|
40
|
+
* This mirrors the validator's queue key so the emitted DAG and its topological
|
|
41
|
+
* order agree on what "earlier" means.
|
|
42
|
+
*/
|
|
43
|
+
export function compareNodes(a, b) {
|
|
44
|
+
const aSeq = a.span?.startSeq;
|
|
45
|
+
const bSeq = b.span?.startSeq;
|
|
46
|
+
if (aSeq !== undefined && bSeq !== undefined) {
|
|
47
|
+
if (aSeq !== bSeq)
|
|
48
|
+
return aSeq < bSeq ? -1 : 1;
|
|
49
|
+
}
|
|
50
|
+
else if (aSeq !== undefined) {
|
|
51
|
+
return -1; // spanned nodes precede synthetics
|
|
52
|
+
}
|
|
53
|
+
else if (bSeq !== undefined) {
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
// Contract key: (startSeq, syntheticOrdinal, id bytes) — byte offset is NOT a
|
|
57
|
+
// tie-break, so the emitted DAG and its topological order agree exactly.
|
|
58
|
+
const aOrd = a.syntheticOrdinal ?? 0;
|
|
59
|
+
const bOrd = b.syntheticOrdinal ?? 0;
|
|
60
|
+
if (aOrd !== bOrd)
|
|
61
|
+
return aOrd < bOrd ? -1 : 1;
|
|
62
|
+
return byBytes(a.id, b.id);
|
|
63
|
+
}
|
|
64
|
+
/** Canonical edge order: from, then to, then kind — a total order. */
|
|
65
|
+
export function compareEdges(a, b) {
|
|
66
|
+
const f = byBytes(a.from, b.from);
|
|
67
|
+
if (f !== 0)
|
|
68
|
+
return f;
|
|
69
|
+
const t = byBytes(a.to, b.to);
|
|
70
|
+
if (t !== 0)
|
|
71
|
+
return t;
|
|
72
|
+
return byBytes(a.kind, b.kind);
|
|
73
|
+
}
|
|
74
|
+
/** Whether two half-open byte ranges in the same session intersect. */
|
|
75
|
+
function spansOverlap(a, b) {
|
|
76
|
+
if (a.sessionId !== b.sessionId)
|
|
77
|
+
return false;
|
|
78
|
+
return a.startByte < b.endByte && b.startByte < a.endByte;
|
|
79
|
+
}
|
|
80
|
+
/** A span is well-formed when both its seq and byte ranges are non-decreasing. */
|
|
81
|
+
function spanIsValid(span) {
|
|
82
|
+
if (span.startSeq > span.endSeq)
|
|
83
|
+
return false;
|
|
84
|
+
if (span.startByte < 0 || span.endByte < 0)
|
|
85
|
+
return false;
|
|
86
|
+
return span.startByte <= span.endByte;
|
|
87
|
+
}
|
|
88
|
+
/** Push a failure code once (codes are a SET, reported in discovery order). */
|
|
89
|
+
function pushCode(codes, code) {
|
|
90
|
+
if (!codes.includes(code))
|
|
91
|
+
codes.push(code);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build a canonical single-session `PromptDagV1`, or reject with the exact
|
|
95
|
+
* structural failure codes (task 2).
|
|
96
|
+
*
|
|
97
|
+
* Every check runs before returning so a caller sees ALL structural problems at
|
|
98
|
+
* once rather than fixing them one round-trip at a time; the codes array is
|
|
99
|
+
* deduplicated and ordered by discovery.
|
|
100
|
+
*/
|
|
101
|
+
export function buildPromptDag(input) {
|
|
102
|
+
const codes = [];
|
|
103
|
+
// ── Node identity + span validity + single-session enforcement ─────────────
|
|
104
|
+
const byId = new Map();
|
|
105
|
+
for (const n of input.nodes) {
|
|
106
|
+
if (byId.has(n.id))
|
|
107
|
+
pushCode(codes, "DAG_DUPLICATE_ID");
|
|
108
|
+
else
|
|
109
|
+
byId.set(n.id, n);
|
|
110
|
+
if (n.kind === "synthetic") {
|
|
111
|
+
// A synthetic node has no source position by definition; carrying a span
|
|
112
|
+
// would make its ordering ambiguous against real source nodes.
|
|
113
|
+
if (n.span !== undefined)
|
|
114
|
+
pushCode(codes, "DAG_INVALID_SPAN");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (n.span === undefined)
|
|
118
|
+
continue; // a spanless non-synthetic orders last
|
|
119
|
+
if (!spanIsValid(n.span))
|
|
120
|
+
pushCode(codes, "DAG_INVALID_SPAN");
|
|
121
|
+
// Single-session: cross-session evidence must be a synthetic citation.
|
|
122
|
+
if (n.span.sessionId !== input.sessionId)
|
|
123
|
+
pushCode(codes, "DAG_MIXED_SESSION");
|
|
124
|
+
}
|
|
125
|
+
// ── Overlapping spans must agree on the digest of the shared bytes ─────────
|
|
126
|
+
const spanned = input.nodes.filter((n) => n.span !== undefined);
|
|
127
|
+
for (let i = 0; i < spanned.length; i++) {
|
|
128
|
+
for (let j = i + 1; j < spanned.length; j++) {
|
|
129
|
+
const a = spanned[i];
|
|
130
|
+
const b = spanned[j];
|
|
131
|
+
if (!spansOverlap(a.span, b.span))
|
|
132
|
+
continue;
|
|
133
|
+
if (a.span.digest !== b.span.digest) {
|
|
134
|
+
pushCode(codes, "DAG_SPAN_DIGEST_CONFLICT");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ── Incompatibility targets must exist ────────────────────────────────────
|
|
139
|
+
for (const n of input.nodes) {
|
|
140
|
+
for (const other of n.incompatibleWith) {
|
|
141
|
+
if (!byId.has(other))
|
|
142
|
+
pushCode(codes, "DAG_UNKNOWN_INCOMPATIBLE");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ── Edge endpoints must exist; tool pairs must be whole ───────────────────
|
|
146
|
+
for (const e of input.edges) {
|
|
147
|
+
const hasFrom = byId.has(e.from);
|
|
148
|
+
const hasTo = byId.has(e.to);
|
|
149
|
+
if (!hasFrom || !hasTo) {
|
|
150
|
+
pushCode(codes, "DAG_MISSING_ENDPOINT");
|
|
151
|
+
// A tool pair missing an endpoint is ALSO a split pair (PREVENT-PI-002):
|
|
152
|
+
// the atomic unit cannot be represented, let alone selected whole.
|
|
153
|
+
if (e.kind === "tool-pair")
|
|
154
|
+
pushCode(codes, "DAG_TOOL_PAIR_SPLIT");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (codes.length > 0)
|
|
158
|
+
return { ok: false, codes };
|
|
159
|
+
const nodes = [...input.nodes].sort(compareNodes);
|
|
160
|
+
const edges = [...input.edges].sort(compareEdges);
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
dag: {
|
|
164
|
+
schema: "prompt-dag-v1",
|
|
165
|
+
sessionId: input.sessionId,
|
|
166
|
+
sourceHighWater: input.sourceHighWater,
|
|
167
|
+
nodes,
|
|
168
|
+
edges,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/prompt-dag/types.ts — the exact single-session `PromptDagV1`
|
|
3
|
+
* schema (VC5A, task 1).
|
|
4
|
+
*
|
|
5
|
+
* A PromptDagV1 is the STRUCTURE a plan is selected over: nodes are prompt
|
|
6
|
+
* candidates (events, exact spans, semantic renderings, synthetic citations) and
|
|
7
|
+
* edges are the ordering/atomicity/exclusivity constraints among them. It is
|
|
8
|
+
* strictly SINGLE-SESSION (CONTRACTS §PromptDagV1): every node span MUST carry
|
|
9
|
+
* `dag.sessionId`; cross-session evidence is represented by a SYNTHETIC node
|
|
10
|
+
* whose payload cites a separately validated source manifest, never by a foreign
|
|
11
|
+
* span.
|
|
12
|
+
*
|
|
13
|
+
* Ordering is a total order by construction so the same DAG always yields the
|
|
14
|
+
* same Kahn sequence:
|
|
15
|
+
*
|
|
16
|
+
* - event/exact/semantic nodes order by their source span (`startSeq`, then
|
|
17
|
+
* `startByte`);
|
|
18
|
+
* - synthetic nodes have NO span and sort after every prerequisite, then by
|
|
19
|
+
* `syntheticOrdinal`, then by node-ID bytes;
|
|
20
|
+
* - the zero-indegree queue key is `(span.startSeq ?? MAX, syntheticOrdinal ??
|
|
21
|
+
* 0, id bytes)` — never object/map iteration order.
|
|
22
|
+
*
|
|
23
|
+
* Pure types + registered conformance IDs: no storage, no console, no network
|
|
24
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Registered DAG conformance ID range (DAG-001..030). The acceptance aggregator
|
|
28
|
+
* reads these rows from the v2 manifest and asserts each returns its manifest
|
|
29
|
+
* `ok`/`code`.
|
|
30
|
+
*/
|
|
31
|
+
export const DAG_IDS = Array.from({ length: 30 }, (_v, i) => `DAG-${String(i + 1).padStart(3, "0")}`);
|
|
32
|
+
/** Named VC5A DAG conformance assertion (the sprint's headline DAG row). */
|
|
33
|
+
export const DAG_NAMED_IDS = ["DAG-CYCLE-001"];
|
|
Binary file
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/planner/manifest.ts — plan manifest identity + pre-provider
|
|
3
|
+
* revalidation (VC5A).
|
|
4
|
+
*
|
|
5
|
+
* Split from `portfolio.ts` (which owns SELECTION) so each file keeps one
|
|
6
|
+
* concern and stays under the 300-line soft limit: this module owns the plan's
|
|
7
|
+
* IDENTITY and the last gate before a provider call.
|
|
8
|
+
*
|
|
9
|
+
* The manifest digest deliberately covers PER-NODE TOKEN COUNTS, which the DAG
|
|
10
|
+
* digest does not: token counts are a planner input rather than DAG structure.
|
|
11
|
+
* That is exactly what makes the sprint's unique failure injection detectable —
|
|
12
|
+
* mutating a node's token count after planning but before validation changes the
|
|
13
|
+
* recomputed manifest digest, so `validatePlanManifest` returns
|
|
14
|
+
* `PLN_MANIFEST_DIGEST_MISMATCH` and the plan never reaches the provider.
|
|
15
|
+
*
|
|
16
|
+
* Pure/deterministic: no storage, no console, no network (PREVENT-PI-004).
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic digest over a plan AND the token counts/utilities it was
|
|
21
|
+
* selected with. Hashing walks `selectedNodeIds` in its stored (sorted) order,
|
|
22
|
+
* so the digest is a pure function of the plan and the candidate facts.
|
|
23
|
+
*
|
|
24
|
+
* A node missing from `candidates` hashes as `-1`, so dropping a candidate is
|
|
25
|
+
* itself a detectable mutation rather than a silently-skipped field.
|
|
26
|
+
*/
|
|
27
|
+
export function planManifestDigest(plan, candidates) {
|
|
28
|
+
const h = createHash("sha256");
|
|
29
|
+
h.update(plan.schema);
|
|
30
|
+
h.update(" ");
|
|
31
|
+
h.update(plan.dagDigest);
|
|
32
|
+
h.update(" ");
|
|
33
|
+
h.update(String(plan.tokenBudget));
|
|
34
|
+
h.update(" ");
|
|
35
|
+
h.update(String(plan.tokenTotal));
|
|
36
|
+
h.update(" ");
|
|
37
|
+
h.update(String(plan.dependencyHighWater));
|
|
38
|
+
const byId = new Map(candidates.map((c) => [c.nodeId, c]));
|
|
39
|
+
for (const id of plan.selectedNodeIds) {
|
|
40
|
+
const c = byId.get(id);
|
|
41
|
+
h.update("");
|
|
42
|
+
h.update(id);
|
|
43
|
+
h.update(" ");
|
|
44
|
+
// Token count is part of the identity — a post-plan mutation breaks it.
|
|
45
|
+
h.update(String(c?.tokenEstimate ?? -1));
|
|
46
|
+
h.update(" ");
|
|
47
|
+
h.update(String(c?.utility ?? -1));
|
|
48
|
+
}
|
|
49
|
+
return h.digest("hex");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Re-validate a plan against the candidates AS THEY STAND NOW, immediately
|
|
53
|
+
* before a provider call. Returns `PLN_MANIFEST_DIGEST_MISMATCH` when the
|
|
54
|
+
* recomputed manifest digest disagrees with the digest pinned at planning time.
|
|
55
|
+
*
|
|
56
|
+
* This is the sprint's unique failure injection: a node token count mutated
|
|
57
|
+
* after planning but before validation is caught here, BEFORE the provider is
|
|
58
|
+
* invoked, rather than producing a prompt whose real cost differs from the
|
|
59
|
+
* admitted budget.
|
|
60
|
+
*/
|
|
61
|
+
export function validatePlanManifest(plan, candidates, pinnedDigest) {
|
|
62
|
+
if (planManifestDigest(plan, candidates) !== pinnedDigest) {
|
|
63
|
+
return { ok: false, code: "PLN_MANIFEST_DIGEST_MISMATCH" };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true };
|
|
66
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/planner/types.ts — `PlanV1` + the budget admission contract
|
|
3
|
+
* (VC5A, task 1).
|
|
4
|
+
*
|
|
5
|
+
* VC5A EXCLUSIVELY OWNS FRAMING + BUDGET ADMISSION (CONTRACTS §plan and
|
|
6
|
+
* closure). VC4C hands over `ClosureResult.mandatoryTokenEstimate`, a
|
|
7
|
+
* CONTENT-ONLY count with no prompt framing, no role tags and no separators, and
|
|
8
|
+
* VC4C never truncates a mandatory node nor reasons about a budget. This module
|
|
9
|
+
* adds the framing cost on top of that content estimate and decides admission:
|
|
10
|
+
*
|
|
11
|
+
* framed(node) = tokenEstimate + framingPerNode
|
|
12
|
+
* mandatoryCost = mandatoryTokenEstimate + framingPerNode * |mandatory|
|
|
13
|
+
* + framingOverhead
|
|
14
|
+
*
|
|
15
|
+
* If `mandatoryCost > tokenBudget` the planner returns
|
|
16
|
+
* `MANDATORY_CLOSURE_OVER_BUDGET` and the adapter demotes to C. Crucially it
|
|
17
|
+
* does so WITHOUT DROPPING EVIDENCE: the mandatory set is returned intact on the
|
|
18
|
+
* failure so the caller can report exactly what did not fit (the sprint bar:
|
|
19
|
+
* "over-budget mandatory closure never drops evidence and always demotes").
|
|
20
|
+
*
|
|
21
|
+
* The framing constants are CONFIGURABLE, never invented magic numbers: a caller
|
|
22
|
+
* supplies the profile it is actually rendering with, and the defaults below are
|
|
23
|
+
* documented as a conservative baseline rather than a measured provider fact.
|
|
24
|
+
*
|
|
25
|
+
* Pure types + registered conformance IDs: no storage, no console, no network
|
|
26
|
+
* (PREVENT-PI-004 / PREVENT-011).
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Conservative default framing baseline. Documented as a BASELINE, not a
|
|
30
|
+
* measured provider constant: a real provider profile (VC5B) overrides it. Kept
|
|
31
|
+
* small and explicit so a default-driven plan is never silently over-optimistic.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_FRAMING = { perNode: 4, overhead: 8 };
|
|
34
|
+
/**
|
|
35
|
+
* Registered PLN conformance ID range (PLN-001..020). The acceptance aggregator
|
|
36
|
+
* reads these rows from the v2 manifest and asserts each returns its manifest
|
|
37
|
+
* `ok`/`code`.
|
|
38
|
+
*/
|
|
39
|
+
export const PLN_IDS = Array.from({ length: 20 }, (_v, i) => `PLN-${String(i + 1).padStart(3, "0")}`);
|
|
40
|
+
/** Named VC5A planner conformance assertions (the sprint's headline rows). */
|
|
41
|
+
export const PLAN_NAMED_IDS = ["PLN-MANDATORY-002", "PLN-TIE-003"];
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-dag/_acceptance-dag.ts — declarative DAG materialization + scenario
|
|
3
|
+
* driver for VC5A acceptance rows.
|
|
4
|
+
*
|
|
5
|
+
* Turns named graph scenarios into real `PromptDagV1` values and drives them
|
|
6
|
+
* through the REAL build/validate logic. Deterministic helpers (eq/shuffle)
|
|
7
|
+
* live in `_acceptance-shuffle.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { buildPromptDag, compareNodes, compareEdges } from "./builder.js";
|
|
11
|
+
import { validatePromptDag, dagDigest } from "./validator.js";
|
|
12
|
+
import { eq, shuffle } from "./_acceptance-shuffle.js";
|
|
13
|
+
function span(sessionId, startSeq, endSeq, startByte, endByte, digest) {
|
|
14
|
+
return { sessionId, startSeq, endSeq, startByte, endByte, digest: `sha256:${digest}` };
|
|
15
|
+
}
|
|
16
|
+
function node(id, kind, opts = {}) {
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
kind,
|
|
20
|
+
payloadDigest: opts.payloadDigest ?? `pd:${id}`,
|
|
21
|
+
incompatibleWith: opts.incompatibleWith ?? [],
|
|
22
|
+
...(opts.span !== undefined ? { span: opts.span } : {}),
|
|
23
|
+
...(opts.syntheticOrdinal !== undefined ? { syntheticOrdinal: opts.syntheticOrdinal } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function edge(from, to, kind) {
|
|
27
|
+
return { from, to, kind };
|
|
28
|
+
}
|
|
29
|
+
const D0 = "a".repeat(64);
|
|
30
|
+
const D1 = "b".repeat(64);
|
|
31
|
+
/** Materialize a named graph into a built PromptDagV1 + a validator order. */
|
|
32
|
+
export function materializeDag(name) {
|
|
33
|
+
const cases = {
|
|
34
|
+
linear: {
|
|
35
|
+
sessionId: "s",
|
|
36
|
+
nodes: [
|
|
37
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
38
|
+
node("b", "exact", { span: span("s", 2n, 2n, 3, 6, D0) }),
|
|
39
|
+
node("c", "semantic", { span: span("s", 3n, 3n, 6, 9, D0) }),
|
|
40
|
+
],
|
|
41
|
+
edges: [edge("a", "b", "depends"), edge("b", "c", "depends")],
|
|
42
|
+
},
|
|
43
|
+
"mixed-session": {
|
|
44
|
+
sessionId: "s",
|
|
45
|
+
nodes: [
|
|
46
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
47
|
+
// A span claiming a foreign session — single-session violation.
|
|
48
|
+
node("b", "exact", { span: span("OTHER", 2n, 2n, 0, 3, D0) }),
|
|
49
|
+
],
|
|
50
|
+
edges: [],
|
|
51
|
+
},
|
|
52
|
+
"duplicate-id": {
|
|
53
|
+
sessionId: "s",
|
|
54
|
+
nodes: [
|
|
55
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
56
|
+
node("a", "exact", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
57
|
+
],
|
|
58
|
+
edges: [],
|
|
59
|
+
},
|
|
60
|
+
"missing-endpoint": {
|
|
61
|
+
sessionId: "s",
|
|
62
|
+
nodes: [node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) })],
|
|
63
|
+
edges: [edge("a", "ghost", "depends")],
|
|
64
|
+
},
|
|
65
|
+
"invalid-span": {
|
|
66
|
+
sessionId: "s",
|
|
67
|
+
nodes: [node("a", "event", { span: span("s", 3n, 1n, 9, 0, D0) })],
|
|
68
|
+
edges: [],
|
|
69
|
+
},
|
|
70
|
+
"synthetic-span": {
|
|
71
|
+
sessionId: "s",
|
|
72
|
+
nodes: [
|
|
73
|
+
node("a", "synthetic", { syntheticOrdinal: 0, span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
74
|
+
],
|
|
75
|
+
edges: [],
|
|
76
|
+
},
|
|
77
|
+
"digest-conflict": {
|
|
78
|
+
sessionId: "s",
|
|
79
|
+
nodes: [
|
|
80
|
+
node("a", "exact", { span: span("s", 1n, 1n, 0, 6, D0) }),
|
|
81
|
+
node("b", "exact", { span: span("s", 2n, 2n, 3, 9, D1) }),
|
|
82
|
+
],
|
|
83
|
+
edges: [],
|
|
84
|
+
},
|
|
85
|
+
"digest-agree": {
|
|
86
|
+
sessionId: "s",
|
|
87
|
+
nodes: [
|
|
88
|
+
node("a", "exact", { span: span("s", 1n, 1n, 0, 6, D0) }),
|
|
89
|
+
node("b", "exact", { span: span("s", 2n, 2n, 3, 9, D0) }),
|
|
90
|
+
],
|
|
91
|
+
edges: [],
|
|
92
|
+
},
|
|
93
|
+
"unknown-incompatible": {
|
|
94
|
+
sessionId: "s",
|
|
95
|
+
nodes: [
|
|
96
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0), incompatibleWith: ["ghost"] }),
|
|
97
|
+
node("b", "event", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
98
|
+
],
|
|
99
|
+
edges: [],
|
|
100
|
+
},
|
|
101
|
+
"tool-pair-split": {
|
|
102
|
+
sessionId: "s",
|
|
103
|
+
nodes: [node("toolcall", "event", { span: span("s", 1n, 1n, 0, 3, D0) })],
|
|
104
|
+
edges: [edge("toolcall", "toolresult", "tool-pair")],
|
|
105
|
+
},
|
|
106
|
+
cycle: {
|
|
107
|
+
sessionId: "s",
|
|
108
|
+
nodes: [
|
|
109
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
110
|
+
node("b", "event", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
111
|
+
node("c", "event", { span: span("s", 3n, 3n, 0, 3, D0) }),
|
|
112
|
+
],
|
|
113
|
+
edges: [edge("b", "a", "depends"), edge("c", "b", "depends"), edge("a", "c", "depends")],
|
|
114
|
+
},
|
|
115
|
+
selfloop: {
|
|
116
|
+
sessionId: "s",
|
|
117
|
+
nodes: [node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) })],
|
|
118
|
+
edges: [edge("a", "a", "depends")],
|
|
119
|
+
},
|
|
120
|
+
"id-tie": {
|
|
121
|
+
sessionId: "s",
|
|
122
|
+
nodes: [
|
|
123
|
+
node("aa", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
124
|
+
node("bb", "event", { span: span("s", 1n, 1n, 3, 6, D1) }),
|
|
125
|
+
node("cc", "event", { span: span("s", 1n, 1n, 6, 9, D0) }),
|
|
126
|
+
],
|
|
127
|
+
edges: [],
|
|
128
|
+
},
|
|
129
|
+
"seq-before-id": {
|
|
130
|
+
sessionId: "s",
|
|
131
|
+
// Earlier seq sorts first even though IDs sort the other way.
|
|
132
|
+
nodes: [
|
|
133
|
+
node("z", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
134
|
+
node("y", "event", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
135
|
+
node("x", "event", { span: span("s", 3n, 3n, 0, 3, D0) }),
|
|
136
|
+
],
|
|
137
|
+
edges: [],
|
|
138
|
+
},
|
|
139
|
+
"synthetic-last": {
|
|
140
|
+
sessionId: "s",
|
|
141
|
+
nodes: [
|
|
142
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
143
|
+
node("b", "event", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
144
|
+
node("syn", "synthetic", { syntheticOrdinal: 0 }),
|
|
145
|
+
],
|
|
146
|
+
edges: [],
|
|
147
|
+
},
|
|
148
|
+
"synthetic-ordinal": {
|
|
149
|
+
sessionId: "s",
|
|
150
|
+
nodes: [
|
|
151
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
152
|
+
node("s1", "synthetic", { syntheticOrdinal: 1 }),
|
|
153
|
+
node("s2", "synthetic", { syntheticOrdinal: 2 }),
|
|
154
|
+
],
|
|
155
|
+
edges: [],
|
|
156
|
+
},
|
|
157
|
+
diamond: {
|
|
158
|
+
sessionId: "s",
|
|
159
|
+
nodes: [
|
|
160
|
+
node("top", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
161
|
+
node("l", "event", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
162
|
+
node("r", "event", { span: span("s", 3n, 3n, 0, 3, D0) }),
|
|
163
|
+
node("bottom", "event", { span: span("s", 4n, 4n, 0, 3, D1) }),
|
|
164
|
+
],
|
|
165
|
+
edges: [
|
|
166
|
+
edge("top", "l", "depends"),
|
|
167
|
+
edge("top", "r", "depends"),
|
|
168
|
+
edge("l", "bottom", "depends"),
|
|
169
|
+
edge("r", "bottom", "depends"),
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
contradicts: {
|
|
173
|
+
sessionId: "s",
|
|
174
|
+
nodes: [
|
|
175
|
+
node("a", "exact", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
176
|
+
node("b", "exact", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
177
|
+
],
|
|
178
|
+
edges: [edge("a", "b", "contradicts")],
|
|
179
|
+
},
|
|
180
|
+
"contradicts-cycle": {
|
|
181
|
+
sessionId: "s",
|
|
182
|
+
nodes: [
|
|
183
|
+
node("a", "exact", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
184
|
+
node("b", "exact", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
185
|
+
],
|
|
186
|
+
// A contradicts edge plus a depends edge: a real cycle would need the
|
|
187
|
+
// depends to form a loop, but here only `a->b` depends, so no cycle.
|
|
188
|
+
edges: [edge("a", "b", "contradicts"), edge("a", "b", "depends")],
|
|
189
|
+
},
|
|
190
|
+
"reversed-precedes": {
|
|
191
|
+
sessionId: "s",
|
|
192
|
+
nodes: [
|
|
193
|
+
node("a", "event", { span: span("s", 2n, 2n, 0, 3, D0) }),
|
|
194
|
+
node("b", "event", { span: span("s", 1n, 1n, 0, 3, D1) }),
|
|
195
|
+
],
|
|
196
|
+
// b's source position is EARLIER than a's, so a->b precedes is backward.
|
|
197
|
+
edges: [edge("a", "b", "precedes")],
|
|
198
|
+
},
|
|
199
|
+
scrambled: {
|
|
200
|
+
sessionId: "s",
|
|
201
|
+
nodes: [
|
|
202
|
+
node("c", "event", { span: span("s", 3n, 3n, 6, 9, D0) }),
|
|
203
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
204
|
+
node("b", "event", { span: span("s", 2n, 2n, 3, 6, D1) }),
|
|
205
|
+
],
|
|
206
|
+
edges: [edge("a", "b", "depends"), edge("b", "c", "depends")],
|
|
207
|
+
},
|
|
208
|
+
empty: { sessionId: "s", nodes: [], edges: [] },
|
|
209
|
+
single: {
|
|
210
|
+
sessionId: "s",
|
|
211
|
+
nodes: [node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) })],
|
|
212
|
+
edges: [],
|
|
213
|
+
},
|
|
214
|
+
disconnected: {
|
|
215
|
+
sessionId: "s",
|
|
216
|
+
nodes: [
|
|
217
|
+
node("a", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
218
|
+
node("b", "event", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
219
|
+
node("x", "event", { span: span("s", 1n, 1n, 3, 6, D0) }),
|
|
220
|
+
node("y", "event", { span: span("s", 2n, 2n, 3, 6, D1) }),
|
|
221
|
+
],
|
|
222
|
+
edges: [],
|
|
223
|
+
},
|
|
224
|
+
toolpair: {
|
|
225
|
+
sessionId: "s",
|
|
226
|
+
nodes: [
|
|
227
|
+
node("toolcall", "event", { span: span("s", 1n, 1n, 0, 3, D0) }),
|
|
228
|
+
node("toolresult", "event", { span: span("s", 2n, 2n, 0, 3, D1) }),
|
|
229
|
+
],
|
|
230
|
+
edges: [edge("toolcall", "toolresult", "tool-pair")],
|
|
231
|
+
},
|
|
232
|
+
chain8: {
|
|
233
|
+
sessionId: "s",
|
|
234
|
+
nodes: [0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => node(`n${i}`, "event", { span: span("s", BigInt(i + 1), BigInt(i + 1), 0, 3, D0) })),
|
|
235
|
+
edges: [0, 1, 2, 3, 4, 5, 6, 7].map((i) => edge(`n${i + 1}`, `n${i}`, "depends")),
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
const g = cases[name];
|
|
239
|
+
assert.ok(g, `dag materializer missing for "${name}"`);
|
|
240
|
+
// Give every node a DISTINCT, non-overlapping byte window so the builder's
|
|
241
|
+
// digest-conflict rule (which only fires on genuinely shared bytes) does not
|
|
242
|
+
// misfire on adjacent ordering-test nodes. The two digest-* graphs intentionally
|
|
243
|
+
// KEEP overlapping spans to exercise that rule, so they are excluded here.
|
|
244
|
+
const overlapIntentional = name === "digest-conflict" || name === "digest-agree";
|
|
245
|
+
const nodes = overlapIntentional
|
|
246
|
+
? g.nodes
|
|
247
|
+
: g.nodes.map((n, i) => n.span === undefined
|
|
248
|
+
? n
|
|
249
|
+
: { ...n, span: { ...n.span, startByte: i * 4, endByte: i * 4 + 4 } });
|
|
250
|
+
const built = buildPromptDag({
|
|
251
|
+
sessionId: g.sessionId,
|
|
252
|
+
sourceHighWater: 100n,
|
|
253
|
+
nodes,
|
|
254
|
+
edges: g.edges,
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
buildOk: built.ok,
|
|
258
|
+
buildCodes: built.ok ? [] : built.codes,
|
|
259
|
+
dag: built.ok
|
|
260
|
+
? built.dag
|
|
261
|
+
: {
|
|
262
|
+
schema: "prompt-dag-v1",
|
|
263
|
+
sessionId: g.sessionId,
|
|
264
|
+
sourceHighWater: 100n,
|
|
265
|
+
nodes: [...g.nodes].sort(compareNodes),
|
|
266
|
+
edges: [...g.edges].sort(compareEdges),
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** Validate a materialized DAG, returning order / failure codes / digest. */
|
|
271
|
+
export function runDagScenario(fx) {
|
|
272
|
+
const { scenario, graph, mutateDigest } = fx.input;
|
|
273
|
+
const base = materializeDag(graph ?? "linear");
|
|
274
|
+
// Structural REJECTION scenarios stop at the builder (no validation). Build
|
|
275
|
+
// ACCEPTANCE scenarios (build-linear / build-digest-agree) succeed and must
|
|
276
|
+
// still be validated for their topological order.
|
|
277
|
+
const structuralReject = [
|
|
278
|
+
"build-mixed-session",
|
|
279
|
+
"build-duplicate-id",
|
|
280
|
+
"build-missing-endpoint",
|
|
281
|
+
"build-invalid-span",
|
|
282
|
+
"build-synthetic-with-span",
|
|
283
|
+
"build-digest-conflict",
|
|
284
|
+
"build-unknown-incompatible",
|
|
285
|
+
"build-tool-pair-split",
|
|
286
|
+
];
|
|
287
|
+
if (structuralReject.includes(scenario)) {
|
|
288
|
+
// Prefer the more specific tool-pair rejection when both it and a generic
|
|
289
|
+
// missing-endpoint code are present (a split tool pair is also missing an
|
|
290
|
+
// endpoint, but the tool-pair split is the precise failure).
|
|
291
|
+
const codes = base.buildCodes;
|
|
292
|
+
const code = codes.includes("DAG_TOOL_PAIR_SPLIT")
|
|
293
|
+
? "DAG_TOOL_PAIR_SPLIT"
|
|
294
|
+
: codes[0];
|
|
295
|
+
return { ok: base.buildOk, code };
|
|
296
|
+
}
|
|
297
|
+
// A built DAG is required for validation scenarios.
|
|
298
|
+
assert.ok(base.buildOk, `${fx.id}: builder must succeed before validation`);
|
|
299
|
+
const dag = base.dag;
|
|
300
|
+
const v = validatePromptDag(dag);
|
|
301
|
+
const digest = dagDigest(dag);
|
|
302
|
+
if (fx.expected.permutationInvariant) {
|
|
303
|
+
// Re-run with a shuffled node/edge array; order + digest must be identical.
|
|
304
|
+
const permNodes = shuffle([...dag.nodes]);
|
|
305
|
+
const permEdges = shuffle([...dag.edges]);
|
|
306
|
+
const perm = buildPromptDag({
|
|
307
|
+
sessionId: dag.sessionId,
|
|
308
|
+
sourceHighWater: dag.sourceHighWater,
|
|
309
|
+
nodes: permNodes,
|
|
310
|
+
edges: permEdges,
|
|
311
|
+
});
|
|
312
|
+
assert.ok(perm.ok, `${fx.id}: permuted build must succeed`);
|
|
313
|
+
const vPerm = validatePromptDag(perm.dag);
|
|
314
|
+
const orderSame = v.ok && vPerm.ok && eq(v.order, vPerm.order);
|
|
315
|
+
const digestSame = dagDigest(perm.dag) === digest;
|
|
316
|
+
return { ok: true, permutationInvariant: orderSame, digestStable: digestSame };
|
|
317
|
+
}
|
|
318
|
+
if (fx.expected.digestStable) {
|
|
319
|
+
const perm = buildPromptDag({
|
|
320
|
+
sessionId: dag.sessionId,
|
|
321
|
+
sourceHighWater: dag.sourceHighWater,
|
|
322
|
+
nodes: shuffle([...dag.nodes]),
|
|
323
|
+
edges: shuffle([...dag.edges]),
|
|
324
|
+
});
|
|
325
|
+
assert.ok(perm.ok);
|
|
326
|
+
return { ok: true, digestStable: dagDigest(perm.dag) === digest };
|
|
327
|
+
}
|
|
328
|
+
if (fx.expected.digestSensitive) {
|
|
329
|
+
const mutated = {
|
|
330
|
+
...dag,
|
|
331
|
+
nodes: dag.nodes.map((n, i) => i === 0 ? { ...n, payloadDigest: `pd:mutated-${n.id}` } : n),
|
|
332
|
+
};
|
|
333
|
+
return { ok: true, digestSensitive: dagDigest(mutated) !== digest };
|
|
334
|
+
}
|
|
335
|
+
if (fx.expected.orderIsTotal) {
|
|
336
|
+
const ok = v.ok && v.order.length === dag.nodes.length && new Set(v.order).size === dag.nodes.length;
|
|
337
|
+
return { ok, orderIsTotal: ok };
|
|
338
|
+
}
|
|
339
|
+
if (fx.expected.orderLength !== undefined) {
|
|
340
|
+
return { ok: v.ok, orderLength: v.ok ? v.order.length : 0 };
|
|
341
|
+
}
|
|
342
|
+
// Plain order / failure-code scenarios.
|
|
343
|
+
if (!v.ok)
|
|
344
|
+
return { ok: false, code: v.codes[0] };
|
|
345
|
+
void mutateDigest;
|
|
346
|
+
return { ok: true, order: [...v.order] };
|
|
347
|
+
}
|