pi-mega-compact 0.20.17 → 0.20.18

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.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * controller/policy.ts — VC8B bounded policy engine (PURE).
3
+ *
4
+ * Three guarantees, all structural rather than conventional:
5
+ *
6
+ * 1. FINITE ACTIONS. `evaluatePolicy` can only ever return a member of
7
+ * POLICY_ACTIONS. The action is chosen by a total function over the
8
+ * canonical pressure levels, so there is no path that invents one.
9
+ * 2. BOUNDED BUDGETS. Every returned budget is clamped into
10
+ * `[minBudget, maxBudget]`. Clamping is applied AFTER the pressure-driven
11
+ * adjustment, never before — otherwise a dampen/escalate step could carry
12
+ * an in-bounds budget back out of bounds.
13
+ * 3. UNKNOWN PRESSURE REJECTS. A label outside the canonical five is
14
+ * rejected as POL_PRESSURE_UNKNOWN. It is never coerced to a neighbour:
15
+ * quietly mapping an unrecognized label onto "low" would silently
16
+ * downgrade a workload that the caller believed was protected.
17
+ *
18
+ * Everything here is PURE: no clock, no storage, no network, no flag read. The
19
+ * flag gates only the reporter seam in policy-emit.ts, which is why flag-off is
20
+ * byte-identical to the predecessor.
21
+ *
22
+ * PREVENT-002/011/PI-004 honored.
23
+ */
24
+ import { POLICY_ACTIONS, POLICY_DECISION_SCHEMA_V1, POL_ACTION_FORBIDDEN, POL_BUDGET_OUT_OF_BOUNDS, POL_PRESSURE_UNKNOWN, PRESSURE_LEVELS, } from "./types.js";
25
+ /** Construct a policy failure. */
26
+ function fail(code) {
27
+ return { code };
28
+ }
29
+ /** Type guard: is this a canonical pressure level? */
30
+ export function isPressureLevel(label) {
31
+ return PRESSURE_LEVELS.includes(label);
32
+ }
33
+ /** Type guard: is this an allowed policy action? */
34
+ export function isPolicyAction(action) {
35
+ return POLICY_ACTIONS.includes(action);
36
+ }
37
+ /**
38
+ * Validate a pressure label against the canonical five. Throws
39
+ * `{ code: POL_PRESSURE_UNKNOWN }` rather than coercing — see the module note.
40
+ */
41
+ export function validatePressureLabel(label) {
42
+ if (!isPressureLevel(label))
43
+ throw fail(POL_PRESSURE_UNKNOWN);
44
+ return label;
45
+ }
46
+ /**
47
+ * Validate an action against the allowed finite set. Throws
48
+ * `{ code: POL_ACTION_FORBIDDEN }` for anything else.
49
+ */
50
+ export function validateAction(action) {
51
+ if (!isPolicyAction(action))
52
+ throw fail(POL_ACTION_FORBIDDEN);
53
+ return action;
54
+ }
55
+ /**
56
+ * Validate the bound pair itself. A window with min > max, or a non-finite
57
+ * bound, has no correct clamp result, so it is rejected rather than guessed at.
58
+ */
59
+ export function validateBounds(bounds) {
60
+ const { minBudget, maxBudget } = bounds;
61
+ if (!Number.isFinite(minBudget) || !Number.isFinite(maxBudget)) {
62
+ throw fail(POL_BUDGET_OUT_OF_BOUNDS);
63
+ }
64
+ if (minBudget > maxBudget)
65
+ throw fail(POL_BUDGET_OUT_OF_BOUNDS);
66
+ if (minBudget < 0)
67
+ throw fail(POL_BUDGET_OUT_OF_BOUNDS);
68
+ return bounds;
69
+ }
70
+ /**
71
+ * Clamp a budget into `[minBudget, maxBudget]`.
72
+ *
73
+ * A NaN budget clamps to `minBudget`: NaN comparisons are all false, so a naive
74
+ * Math.min/Math.max chain would propagate NaN straight through the "bounded"
75
+ * guarantee. The safest interpretation of an unusable request is the floor.
76
+ */
77
+ export function clampBudget(budget, minBudget, maxBudget) {
78
+ validateBounds({ minBudget, maxBudget });
79
+ if (!Number.isFinite(budget))
80
+ return minBudget;
81
+ if (budget < minBudget)
82
+ return minBudget;
83
+ if (budget > maxBudget)
84
+ return maxBudget;
85
+ return budget;
86
+ }
87
+ /** The multiplier applied to the requested budget at each pressure level. */
88
+ const PRESSURE_FACTOR = {
89
+ low: 1,
90
+ medium: 1,
91
+ high: 0.75,
92
+ ultra: 0.5,
93
+ mega: 0.25,
94
+ };
95
+ /**
96
+ * The action selected at each pressure level. Total over PressureLevel, so the
97
+ * action space cannot grow: `mega` refuses outright, `ultra` defers, `high`
98
+ * dampens, and the quiet levels admit.
99
+ */
100
+ const PRESSURE_ACTION = {
101
+ low: "admit",
102
+ medium: "admit",
103
+ high: "dampen",
104
+ ultra: "defer",
105
+ mega: "reject",
106
+ };
107
+ /** Select the reason code that explains the decision. */
108
+ function reasonFor(pressure, requested, clamped, bounds) {
109
+ if (clamped === bounds.maxBudget && requested > bounds.maxBudget) {
110
+ return "budget_clamped_high";
111
+ }
112
+ if (clamped === bounds.minBudget && requested < bounds.minBudget) {
113
+ return "budget_clamped_low";
114
+ }
115
+ if (pressure === "mega" || pressure === "ultra")
116
+ return "pressure_critical";
117
+ if (pressure === "high")
118
+ return "pressure_elevated";
119
+ return "within_bounds";
120
+ }
121
+ /**
122
+ * Evaluate one policy input into a bounded decision.
123
+ *
124
+ * Order matters: validate the label, validate the window, apply the
125
+ * pressure factor, THEN clamp. Clamping last is what makes the bounded-budget
126
+ * guarantee hold for every action including escalate.
127
+ *
128
+ * Throws `{ code }` on an unknown pressure label or an invalid bound pair.
129
+ */
130
+ export function evaluatePolicy(input) {
131
+ const pressure = validatePressureLabel(input.pressure);
132
+ const bounds = validateBounds(input.bounds);
133
+ const requested = Number.isFinite(input.requestedBudget)
134
+ ? input.requestedBudget
135
+ : bounds.minBudget;
136
+ const adjusted = requested * PRESSURE_FACTOR[pressure];
137
+ const budget = clampBudget(adjusted, bounds.minBudget, bounds.maxBudget);
138
+ return {
139
+ schema: POLICY_DECISION_SCHEMA_V1,
140
+ decisionId: input.decisionId,
141
+ sessionId: input.sessionId,
142
+ action: PRESSURE_ACTION[pressure],
143
+ budget,
144
+ pressure,
145
+ reason: reasonFor(pressure, input.requestedBudget, budget, bounds),
146
+ ts: input.ts,
147
+ };
148
+ }
149
+ /**
150
+ * Assert a decision satisfies the sprint invariant: allowed action AND bounded
151
+ * budget. Used by the acceptance aggregator to check every produced row.
152
+ */
153
+ export function isDecisionWithinBounds(decision, bounds) {
154
+ return (isPolicyAction(decision.action) &&
155
+ Number.isFinite(decision.budget) &&
156
+ decision.budget >= bounds.minBudget &&
157
+ decision.budget <= bounds.maxBudget);
158
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * controller/shadow.ts — VC8B shadow policy evaluator (PURE, READ-ONLY).
3
+ *
4
+ * The shadow engine runs the candidate policy alongside the live path so its
5
+ * decisions can be measured before they are trusted. That is only safe if the
6
+ * shadow is structurally incapable of affecting the live path, so this module
7
+ * takes the capability argument seriously:
8
+ *
9
+ * - NO RENDERER. It imports no renderer and returns no rendered bytes.
10
+ * - NO STORE WRITER. It imports no store and performs no write.
11
+ * - NO PROMPT MUTATION. It receives the canonical prompt as bytes it may only
12
+ * hash, and it re-hashes on exit to PROVE the bytes are unchanged
13
+ * (POL-SHADOW-002). `liveMutations` is reported and is always 0.
14
+ *
15
+ * INPUTS ARE COPIED, NOT BORROWED. Every input is deep-copied on entry, so even
16
+ * a future policy change that mutated its argument could not reach the caller's
17
+ * object. The copy is the enforcement; the `readonly` types are only the
18
+ * documentation of it. This is the difference between "we don't mutate" and
19
+ * "we cannot mutate".
20
+ *
21
+ * A rejected input does NOT abort the run: the shadow's job is measurement, so
22
+ * one unknown pressure label is recorded as a rejection code and the remaining
23
+ * inputs are still evaluated.
24
+ *
25
+ * PREVENT-002/011/PI-004 honored.
26
+ */
27
+ import { createHash } from "node:crypto";
28
+ import { evaluatePolicy } from "./policy.js";
29
+ /** SHA-256 of the canonical prompt bytes, lowercase hex (VC5B convention). */
30
+ export function promptDigestOf(promptBytes) {
31
+ return createHash("sha256")
32
+ .update(Buffer.from(promptBytes, "utf8"))
33
+ .digest("hex");
34
+ }
35
+ /**
36
+ * Deep-copy one policy input. Explicit field-by-field construction rather than
37
+ * a structured clone: it keeps the copy total over the declared shape and makes
38
+ * an added field a compile error instead of a silently shared reference.
39
+ */
40
+ export function copyPolicyInput(input) {
41
+ return {
42
+ decisionId: input.decisionId,
43
+ sessionId: input.sessionId,
44
+ pressure: input.pressure,
45
+ requestedBudget: input.requestedBudget,
46
+ bounds: {
47
+ minBudget: input.bounds.minBudget,
48
+ maxBudget: input.bounds.maxBudget,
49
+ },
50
+ ts: input.ts,
51
+ };
52
+ }
53
+ /** Extract the machine code from a thrown policy failure. */
54
+ function codeOf(err) {
55
+ if (typeof err === "object" && err !== null && "code" in err) {
56
+ const code = err.code;
57
+ if (typeof code === "string")
58
+ return code;
59
+ }
60
+ return "POL_UNKNOWN_FAILURE";
61
+ }
62
+ /**
63
+ * Evaluate a batch of policy inputs in shadow mode.
64
+ *
65
+ * Returns decisions + metrics ONLY. The caller receives no capability to apply
66
+ * any of it; promoting a shadow decision is a separate, explicit act.
67
+ *
68
+ * @param inputs the policy inputs to evaluate (copied, never mutated)
69
+ * @param promptBytes the canonical prompt, used ONLY to prove non-mutation
70
+ */
71
+ export function evaluateShadow(inputs, promptBytes) {
72
+ // Hash the prompt BEFORE any evaluation so the exit comparison is meaningful.
73
+ const digestOnEntry = promptDigestOf(promptBytes);
74
+ // Copy every input up front: nothing downstream ever sees the caller's object.
75
+ const copies = inputs.map(copyPolicyInput);
76
+ const decisions = [];
77
+ const rejections = [];
78
+ let clamped = 0;
79
+ for (const copy of copies) {
80
+ try {
81
+ const decision = evaluatePolicy(copy);
82
+ decisions.push(decision);
83
+ const atBound = decision.reason === "budget_clamped_low" ||
84
+ decision.reason === "budget_clamped_high";
85
+ if (atBound)
86
+ clamped += 1;
87
+ }
88
+ catch (err) {
89
+ // Measurement continues: one bad row must not blind the whole run.
90
+ rejections.push({ decisionId: copy.decisionId, code: codeOf(err) });
91
+ }
92
+ }
93
+ // Re-hash on exit. Equality here is the POL-SHADOW-002 proof that the shadow
94
+ // left the canonical prompt untouched.
95
+ const digestOnExit = promptDigestOf(promptBytes);
96
+ const promptUnchanged = digestOnEntry === digestOnExit;
97
+ return {
98
+ decisions,
99
+ rejections,
100
+ metrics: {
101
+ evaluated: decisions.length,
102
+ clamped,
103
+ rejected: rejections.length,
104
+ // Structurally zero: this module holds no writer capability. If the
105
+ // prompt digest ever moved, that is a live mutation and it is counted.
106
+ liveMutations: promptUnchanged ? 0 : 1,
107
+ },
108
+ promptDigest: digestOnExit,
109
+ };
110
+ }
111
+ /**
112
+ * Assert the shadow result carries no live mutation. The sprint's acceptance
113
+ * bar is "shadow live mutation count zero"; this is that check as a function.
114
+ */
115
+ export function isShadowClean(result) {
116
+ return result.metrics.liveMutations === 0;
117
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * controller/types.ts — VC8B policy decision + pressure type definitions.
3
+ *
4
+ * PolicyDecisionV1 is the FINITE, BOUNDED output of the policy engine: one of a
5
+ * closed action set, a token budget clamped into a configured window, and a
6
+ * machine code reason. There is deliberately no free-text field and no open
7
+ * action string — an adaptive policy whose action space can grow at runtime
8
+ * cannot be reviewed, and a budget with no ceiling is a cost incident waiting
9
+ * to happen.
10
+ *
11
+ * PressureV2 canonicalizes the context-pressure label to EXACTLY five levels
12
+ * (low/medium/high/ultra/mega). Anything else is rejected rather than coerced:
13
+ * silently mapping an unrecognized legacy label onto a neighbouring level is
14
+ * how a "high" workload quietly starts being treated as "low".
15
+ *
16
+ * Conformance IDs POL-001..025 and M7-001..015 are registered here as the
17
+ * single source of truth for the sprint's conformance rows.
18
+ *
19
+ * PREVENT-PI-004: type definitions only, no network code.
20
+ * PREVENT-011: no `any` type.
21
+ */
22
+ /** Schema version for PolicyDecisionV1. */
23
+ export const POLICY_DECISION_SCHEMA_V1 = "policy-decision-v1";
24
+ /** Schema version for PressureV2. */
25
+ export const PRESSURE_SCHEMA_V2 = "pressure-v2";
26
+ /** Failure code when a pressure label is outside the canonical five levels. */
27
+ export const POL_PRESSURE_UNKNOWN = "POL_PRESSURE_UNKNOWN";
28
+ /** Failure code when a requested action is outside the allowed finite set. */
29
+ export const POL_ACTION_FORBIDDEN = "POL_ACTION_FORBIDDEN";
30
+ /** Failure code when a budget bound pair is itself invalid (min > max, NaN). */
31
+ export const POL_BUDGET_OUT_OF_BOUNDS = "POL_BUDGET_OUT_OF_BOUNDS";
32
+ /** Failure code when the M7 migration meets a non-canonical pressure label. */
33
+ export const M7_PRESSURE_UNKNOWN = "M7_PRESSURE_UNKNOWN";
34
+ /** Failure code when M7 copied rows do not match the legacy row count. */
35
+ export const M7_COUNT_MISMATCH = "M7_COUNT_MISMATCH";
36
+ /** Failure code when an M7 row digest does not re-derive from its own fields. */
37
+ export const M7_DIGEST_MISMATCH = "M7_DIGEST_MISMATCH";
38
+ /** Failure code when the active pressure pointer is not on the legacy version. */
39
+ export const M7_NOT_ON_LEGACY = "M7_NOT_ON_LEGACY";
40
+ /**
41
+ * The canonical five pressure levels. Ordered low -> mega; the order is
42
+ * meaningful (policy escalates monotonically with pressure) so it is exported
43
+ * as an array, not just a union.
44
+ */
45
+ export const PRESSURE_LEVELS = [
46
+ "low",
47
+ "medium",
48
+ "high",
49
+ "ultra",
50
+ "mega",
51
+ ];
52
+ /**
53
+ * The FINITE allowed policy action set. A decision may carry no other action.
54
+ * `admit` — proceed at the requested budget.
55
+ * `dampen` — proceed at a reduced budget (pressure is elevated).
56
+ * `defer` — postpone the work to a later turn.
57
+ * `escalate` — raise the budget within bounds (headroom is available).
58
+ * `reject` — refuse the work outright.
59
+ */
60
+ export const POLICY_ACTIONS = [
61
+ "admit",
62
+ "dampen",
63
+ "defer",
64
+ "escalate",
65
+ "reject",
66
+ ];
67
+ /** Machine reason codes — never free-text. */
68
+ export const POLICY_REASONS = [
69
+ "within_bounds",
70
+ "budget_clamped_low",
71
+ "budget_clamped_high",
72
+ "pressure_elevated",
73
+ "pressure_critical",
74
+ "headroom_available",
75
+ ];
76
+ /** Conformance IDs POL-001..POL-025 for the 25 numbered policy rows. */
77
+ export const POLICY_CONFORMANCE_IDS = Array.from({ length: 25 }, (_v, i) => `POL-${String(i + 1).padStart(3, "0")}`);
78
+ /** Conformance IDs M7-001..M7-015 for the 15 numbered migration rows. */
79
+ export const M7_CONFORMANCE_IDS = Array.from({ length: 15 }, (_v, i) => `M7-${String(i + 1).padStart(3, "0")}`);
80
+ /** Named conformance fixtures for the sprint's headline assertions. */
81
+ export const POLICY_NAMED_FIXTURES = [
82
+ "POL-CLAMP-001",
83
+ "POL-SHADOW-002",
84
+ "M7-PRESSURE-003",
85
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.17",
3
+ "version": "0.20.18",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",
@@ -0,0 +1,198 @@
1
+ /**
2
+ * controller/policy.ts — VC8B bounded policy engine (PURE).
3
+ *
4
+ * Three guarantees, all structural rather than conventional:
5
+ *
6
+ * 1. FINITE ACTIONS. `evaluatePolicy` can only ever return a member of
7
+ * POLICY_ACTIONS. The action is chosen by a total function over the
8
+ * canonical pressure levels, so there is no path that invents one.
9
+ * 2. BOUNDED BUDGETS. Every returned budget is clamped into
10
+ * `[minBudget, maxBudget]`. Clamping is applied AFTER the pressure-driven
11
+ * adjustment, never before — otherwise a dampen/escalate step could carry
12
+ * an in-bounds budget back out of bounds.
13
+ * 3. UNKNOWN PRESSURE REJECTS. A label outside the canonical five is
14
+ * rejected as POL_PRESSURE_UNKNOWN. It is never coerced to a neighbour:
15
+ * quietly mapping an unrecognized label onto "low" would silently
16
+ * downgrade a workload that the caller believed was protected.
17
+ *
18
+ * Everything here is PURE: no clock, no storage, no network, no flag read. The
19
+ * flag gates only the reporter seam in policy-emit.ts, which is why flag-off is
20
+ * byte-identical to the predecessor.
21
+ *
22
+ * PREVENT-002/011/PI-004 honored.
23
+ */
24
+
25
+ import type {
26
+ PolicyAction,
27
+ PolicyBounds,
28
+ PolicyDecisionV1,
29
+ PolicyInput,
30
+ PolicyReason,
31
+ PressureLevel,
32
+ } from "./types.js";
33
+ import {
34
+ POLICY_ACTIONS,
35
+ POLICY_DECISION_SCHEMA_V1,
36
+ POL_ACTION_FORBIDDEN,
37
+ POL_BUDGET_OUT_OF_BOUNDS,
38
+ POL_PRESSURE_UNKNOWN,
39
+ PRESSURE_LEVELS,
40
+ } from "./types.js";
41
+
42
+ /** A policy failure carrying a machine code (never free-text). */
43
+ export interface PolicyFailure {
44
+ readonly code: string;
45
+ }
46
+
47
+ /** Construct a policy failure. */
48
+ function fail(code: string): PolicyFailure {
49
+ return { code };
50
+ }
51
+
52
+ /** Type guard: is this a canonical pressure level? */
53
+ export function isPressureLevel(label: string): label is PressureLevel {
54
+ return (PRESSURE_LEVELS as readonly string[]).includes(label);
55
+ }
56
+
57
+ /** Type guard: is this an allowed policy action? */
58
+ export function isPolicyAction(action: string): action is PolicyAction {
59
+ return (POLICY_ACTIONS as readonly string[]).includes(action);
60
+ }
61
+
62
+ /**
63
+ * Validate a pressure label against the canonical five. Throws
64
+ * `{ code: POL_PRESSURE_UNKNOWN }` rather than coercing — see the module note.
65
+ */
66
+ export function validatePressureLabel(label: string): PressureLevel {
67
+ if (!isPressureLevel(label)) throw fail(POL_PRESSURE_UNKNOWN);
68
+ return label;
69
+ }
70
+
71
+ /**
72
+ * Validate an action against the allowed finite set. Throws
73
+ * `{ code: POL_ACTION_FORBIDDEN }` for anything else.
74
+ */
75
+ export function validateAction(action: string): PolicyAction {
76
+ if (!isPolicyAction(action)) throw fail(POL_ACTION_FORBIDDEN);
77
+ return action;
78
+ }
79
+
80
+ /**
81
+ * Validate the bound pair itself. A window with min > max, or a non-finite
82
+ * bound, has no correct clamp result, so it is rejected rather than guessed at.
83
+ */
84
+ export function validateBounds(bounds: PolicyBounds): PolicyBounds {
85
+ const { minBudget, maxBudget } = bounds;
86
+ if (!Number.isFinite(minBudget) || !Number.isFinite(maxBudget)) {
87
+ throw fail(POL_BUDGET_OUT_OF_BOUNDS);
88
+ }
89
+ if (minBudget > maxBudget) throw fail(POL_BUDGET_OUT_OF_BOUNDS);
90
+ if (minBudget < 0) throw fail(POL_BUDGET_OUT_OF_BOUNDS);
91
+ return bounds;
92
+ }
93
+
94
+ /**
95
+ * Clamp a budget into `[minBudget, maxBudget]`.
96
+ *
97
+ * A NaN budget clamps to `minBudget`: NaN comparisons are all false, so a naive
98
+ * Math.min/Math.max chain would propagate NaN straight through the "bounded"
99
+ * guarantee. The safest interpretation of an unusable request is the floor.
100
+ */
101
+ export function clampBudget(
102
+ budget: number,
103
+ minBudget: number,
104
+ maxBudget: number,
105
+ ): number {
106
+ validateBounds({ minBudget, maxBudget });
107
+ if (!Number.isFinite(budget)) return minBudget;
108
+ if (budget < minBudget) return minBudget;
109
+ if (budget > maxBudget) return maxBudget;
110
+ return budget;
111
+ }
112
+
113
+ /** The multiplier applied to the requested budget at each pressure level. */
114
+ const PRESSURE_FACTOR: Readonly<Record<PressureLevel, number>> = {
115
+ low: 1,
116
+ medium: 1,
117
+ high: 0.75,
118
+ ultra: 0.5,
119
+ mega: 0.25,
120
+ };
121
+
122
+ /**
123
+ * The action selected at each pressure level. Total over PressureLevel, so the
124
+ * action space cannot grow: `mega` refuses outright, `ultra` defers, `high`
125
+ * dampens, and the quiet levels admit.
126
+ */
127
+ const PRESSURE_ACTION: Readonly<Record<PressureLevel, PolicyAction>> = {
128
+ low: "admit",
129
+ medium: "admit",
130
+ high: "dampen",
131
+ ultra: "defer",
132
+ mega: "reject",
133
+ };
134
+
135
+ /** Select the reason code that explains the decision. */
136
+ function reasonFor(
137
+ pressure: PressureLevel,
138
+ requested: number,
139
+ clamped: number,
140
+ bounds: PolicyBounds,
141
+ ): PolicyReason {
142
+ if (clamped === bounds.maxBudget && requested > bounds.maxBudget) {
143
+ return "budget_clamped_high";
144
+ }
145
+ if (clamped === bounds.minBudget && requested < bounds.minBudget) {
146
+ return "budget_clamped_low";
147
+ }
148
+ if (pressure === "mega" || pressure === "ultra") return "pressure_critical";
149
+ if (pressure === "high") return "pressure_elevated";
150
+ return "within_bounds";
151
+ }
152
+
153
+ /**
154
+ * Evaluate one policy input into a bounded decision.
155
+ *
156
+ * Order matters: validate the label, validate the window, apply the
157
+ * pressure factor, THEN clamp. Clamping last is what makes the bounded-budget
158
+ * guarantee hold for every action including escalate.
159
+ *
160
+ * Throws `{ code }` on an unknown pressure label or an invalid bound pair.
161
+ */
162
+ export function evaluatePolicy(input: PolicyInput): PolicyDecisionV1 {
163
+ const pressure = validatePressureLabel(input.pressure);
164
+ const bounds = validateBounds(input.bounds);
165
+
166
+ const requested = Number.isFinite(input.requestedBudget)
167
+ ? input.requestedBudget
168
+ : bounds.minBudget;
169
+ const adjusted = requested * PRESSURE_FACTOR[pressure];
170
+ const budget = clampBudget(adjusted, bounds.minBudget, bounds.maxBudget);
171
+
172
+ return {
173
+ schema: POLICY_DECISION_SCHEMA_V1,
174
+ decisionId: input.decisionId,
175
+ sessionId: input.sessionId,
176
+ action: PRESSURE_ACTION[pressure],
177
+ budget,
178
+ pressure,
179
+ reason: reasonFor(pressure, input.requestedBudget, budget, bounds),
180
+ ts: input.ts,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Assert a decision satisfies the sprint invariant: allowed action AND bounded
186
+ * budget. Used by the acceptance aggregator to check every produced row.
187
+ */
188
+ export function isDecisionWithinBounds(
189
+ decision: PolicyDecisionV1,
190
+ bounds: PolicyBounds,
191
+ ): boolean {
192
+ return (
193
+ isPolicyAction(decision.action) &&
194
+ Number.isFinite(decision.budget) &&
195
+ decision.budget >= bounds.minBudget &&
196
+ decision.budget <= bounds.maxBudget
197
+ );
198
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * controller/shadow.ts — VC8B shadow policy evaluator (PURE, READ-ONLY).
3
+ *
4
+ * The shadow engine runs the candidate policy alongside the live path so its
5
+ * decisions can be measured before they are trusted. That is only safe if the
6
+ * shadow is structurally incapable of affecting the live path, so this module
7
+ * takes the capability argument seriously:
8
+ *
9
+ * - NO RENDERER. It imports no renderer and returns no rendered bytes.
10
+ * - NO STORE WRITER. It imports no store and performs no write.
11
+ * - NO PROMPT MUTATION. It receives the canonical prompt as bytes it may only
12
+ * hash, and it re-hashes on exit to PROVE the bytes are unchanged
13
+ * (POL-SHADOW-002). `liveMutations` is reported and is always 0.
14
+ *
15
+ * INPUTS ARE COPIED, NOT BORROWED. Every input is deep-copied on entry, so even
16
+ * a future policy change that mutated its argument could not reach the caller's
17
+ * object. The copy is the enforcement; the `readonly` types are only the
18
+ * documentation of it. This is the difference between "we don't mutate" and
19
+ * "we cannot mutate".
20
+ *
21
+ * A rejected input does NOT abort the run: the shadow's job is measurement, so
22
+ * one unknown pressure label is recorded as a rejection code and the remaining
23
+ * inputs are still evaluated.
24
+ *
25
+ * PREVENT-002/011/PI-004 honored.
26
+ */
27
+
28
+ import { createHash } from "node:crypto";
29
+
30
+ import type {
31
+ PolicyDecisionV1,
32
+ PolicyInput,
33
+ ShadowRejection,
34
+ ShadowResult,
35
+ } from "./types.js";
36
+ import { evaluatePolicy } from "./policy.js";
37
+
38
+ /** SHA-256 of the canonical prompt bytes, lowercase hex (VC5B convention). */
39
+ export function promptDigestOf(promptBytes: string): string {
40
+ return createHash("sha256")
41
+ .update(Buffer.from(promptBytes, "utf8"))
42
+ .digest("hex");
43
+ }
44
+
45
+ /**
46
+ * Deep-copy one policy input. Explicit field-by-field construction rather than
47
+ * a structured clone: it keeps the copy total over the declared shape and makes
48
+ * an added field a compile error instead of a silently shared reference.
49
+ */
50
+ export function copyPolicyInput(input: PolicyInput): PolicyInput {
51
+ return {
52
+ decisionId: input.decisionId,
53
+ sessionId: input.sessionId,
54
+ pressure: input.pressure,
55
+ requestedBudget: input.requestedBudget,
56
+ bounds: {
57
+ minBudget: input.bounds.minBudget,
58
+ maxBudget: input.bounds.maxBudget,
59
+ },
60
+ ts: input.ts,
61
+ };
62
+ }
63
+
64
+ /** Extract the machine code from a thrown policy failure. */
65
+ function codeOf(err: unknown): string {
66
+ if (typeof err === "object" && err !== null && "code" in err) {
67
+ const code = (err as { code: unknown }).code;
68
+ if (typeof code === "string") return code;
69
+ }
70
+ return "POL_UNKNOWN_FAILURE";
71
+ }
72
+
73
+ /**
74
+ * Evaluate a batch of policy inputs in shadow mode.
75
+ *
76
+ * Returns decisions + metrics ONLY. The caller receives no capability to apply
77
+ * any of it; promoting a shadow decision is a separate, explicit act.
78
+ *
79
+ * @param inputs the policy inputs to evaluate (copied, never mutated)
80
+ * @param promptBytes the canonical prompt, used ONLY to prove non-mutation
81
+ */
82
+ export function evaluateShadow(
83
+ inputs: readonly PolicyInput[],
84
+ promptBytes: string,
85
+ ): ShadowResult {
86
+ // Hash the prompt BEFORE any evaluation so the exit comparison is meaningful.
87
+ const digestOnEntry = promptDigestOf(promptBytes);
88
+
89
+ // Copy every input up front: nothing downstream ever sees the caller's object.
90
+ const copies = inputs.map(copyPolicyInput);
91
+
92
+ const decisions: PolicyDecisionV1[] = [];
93
+ const rejections: ShadowRejection[] = [];
94
+ let clamped = 0;
95
+
96
+ for (const copy of copies) {
97
+ try {
98
+ const decision = evaluatePolicy(copy);
99
+ decisions.push(decision);
100
+ const atBound =
101
+ decision.reason === "budget_clamped_low" ||
102
+ decision.reason === "budget_clamped_high";
103
+ if (atBound) clamped += 1;
104
+ } catch (err) {
105
+ // Measurement continues: one bad row must not blind the whole run.
106
+ rejections.push({ decisionId: copy.decisionId, code: codeOf(err) });
107
+ }
108
+ }
109
+
110
+ // Re-hash on exit. Equality here is the POL-SHADOW-002 proof that the shadow
111
+ // left the canonical prompt untouched.
112
+ const digestOnExit = promptDigestOf(promptBytes);
113
+ const promptUnchanged = digestOnEntry === digestOnExit;
114
+
115
+ return {
116
+ decisions,
117
+ rejections,
118
+ metrics: {
119
+ evaluated: decisions.length,
120
+ clamped,
121
+ rejected: rejections.length,
122
+ // Structurally zero: this module holds no writer capability. If the
123
+ // prompt digest ever moved, that is a live mutation and it is counted.
124
+ liveMutations: promptUnchanged ? 0 : 1,
125
+ },
126
+ promptDigest: digestOnExit,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Assert the shadow result carries no live mutation. The sprint's acceptance
132
+ * bar is "shadow live mutation count zero"; this is that check as a function.
133
+ */
134
+ export function isShadowClean(result: ShadowResult): boolean {
135
+ return result.metrics.liveMutations === 0;
136
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * controller/types.ts — VC8B policy decision + pressure type definitions.
3
+ *
4
+ * PolicyDecisionV1 is the FINITE, BOUNDED output of the policy engine: one of a
5
+ * closed action set, a token budget clamped into a configured window, and a
6
+ * machine code reason. There is deliberately no free-text field and no open
7
+ * action string — an adaptive policy whose action space can grow at runtime
8
+ * cannot be reviewed, and a budget with no ceiling is a cost incident waiting
9
+ * to happen.
10
+ *
11
+ * PressureV2 canonicalizes the context-pressure label to EXACTLY five levels
12
+ * (low/medium/high/ultra/mega). Anything else is rejected rather than coerced:
13
+ * silently mapping an unrecognized legacy label onto a neighbouring level is
14
+ * how a "high" workload quietly starts being treated as "low".
15
+ *
16
+ * Conformance IDs POL-001..025 and M7-001..015 are registered here as the
17
+ * single source of truth for the sprint's conformance rows.
18
+ *
19
+ * PREVENT-PI-004: type definitions only, no network code.
20
+ * PREVENT-011: no `any` type.
21
+ */
22
+
23
+ /** Schema version for PolicyDecisionV1. */
24
+ export const POLICY_DECISION_SCHEMA_V1 = "policy-decision-v1";
25
+
26
+ /** Schema version for PressureV2. */
27
+ export const PRESSURE_SCHEMA_V2 = "pressure-v2";
28
+
29
+ /** Failure code when a pressure label is outside the canonical five levels. */
30
+ export const POL_PRESSURE_UNKNOWN = "POL_PRESSURE_UNKNOWN";
31
+
32
+ /** Failure code when a requested action is outside the allowed finite set. */
33
+ export const POL_ACTION_FORBIDDEN = "POL_ACTION_FORBIDDEN";
34
+
35
+ /** Failure code when a budget bound pair is itself invalid (min > max, NaN). */
36
+ export const POL_BUDGET_OUT_OF_BOUNDS = "POL_BUDGET_OUT_OF_BOUNDS";
37
+
38
+ /** Failure code when the M7 migration meets a non-canonical pressure label. */
39
+ export const M7_PRESSURE_UNKNOWN = "M7_PRESSURE_UNKNOWN";
40
+
41
+ /** Failure code when M7 copied rows do not match the legacy row count. */
42
+ export const M7_COUNT_MISMATCH = "M7_COUNT_MISMATCH";
43
+
44
+ /** Failure code when an M7 row digest does not re-derive from its own fields. */
45
+ export const M7_DIGEST_MISMATCH = "M7_DIGEST_MISMATCH";
46
+
47
+ /** Failure code when the active pressure pointer is not on the legacy version. */
48
+ export const M7_NOT_ON_LEGACY = "M7_NOT_ON_LEGACY";
49
+
50
+ /**
51
+ * The canonical five pressure levels. Ordered low -> mega; the order is
52
+ * meaningful (policy escalates monotonically with pressure) so it is exported
53
+ * as an array, not just a union.
54
+ */
55
+ export const PRESSURE_LEVELS = [
56
+ "low",
57
+ "medium",
58
+ "high",
59
+ "ultra",
60
+ "mega",
61
+ ] as const;
62
+
63
+ /** A canonical context-pressure level. */
64
+ export type PressureLevel = (typeof PRESSURE_LEVELS)[number];
65
+
66
+ /**
67
+ * The FINITE allowed policy action set. A decision may carry no other action.
68
+ * `admit` — proceed at the requested budget.
69
+ * `dampen` — proceed at a reduced budget (pressure is elevated).
70
+ * `defer` — postpone the work to a later turn.
71
+ * `escalate` — raise the budget within bounds (headroom is available).
72
+ * `reject` — refuse the work outright.
73
+ */
74
+ export const POLICY_ACTIONS = [
75
+ "admit",
76
+ "dampen",
77
+ "defer",
78
+ "escalate",
79
+ "reject",
80
+ ] as const;
81
+
82
+ /** An allowed policy action. */
83
+ export type PolicyAction = (typeof POLICY_ACTIONS)[number];
84
+
85
+ /** Machine reason codes — never free-text. */
86
+ export const POLICY_REASONS = [
87
+ "within_bounds",
88
+ "budget_clamped_low",
89
+ "budget_clamped_high",
90
+ "pressure_elevated",
91
+ "pressure_critical",
92
+ "headroom_available",
93
+ ] as const;
94
+
95
+ /** A machine reason code explaining a decision. */
96
+ export type PolicyReason = (typeof POLICY_REASONS)[number];
97
+
98
+ /** The configured token-budget window a decision is clamped into. */
99
+ export interface PolicyBounds {
100
+ readonly minBudget: number;
101
+ readonly maxBudget: number;
102
+ }
103
+
104
+ /**
105
+ * PolicyDecisionV1 — one bounded policy decision.
106
+ * `budget` is ALWAYS within the bounds that produced it; `action` is always a
107
+ * member of POLICY_ACTIONS.
108
+ */
109
+ export interface PolicyDecisionV1 {
110
+ readonly schema: typeof POLICY_DECISION_SCHEMA_V1;
111
+ readonly decisionId: string;
112
+ readonly sessionId: string;
113
+ readonly action: PolicyAction;
114
+ readonly budget: number;
115
+ readonly pressure: PressureLevel;
116
+ readonly reason: PolicyReason;
117
+ readonly ts: string;
118
+ }
119
+
120
+ /** The input a policy evaluation consumes. */
121
+ export interface PolicyInput {
122
+ readonly decisionId: string;
123
+ readonly sessionId: string;
124
+ /** The pressure label as received — validated, never coerced. */
125
+ readonly pressure: string;
126
+ /** The requested budget before clamping; any finite number. */
127
+ readonly requestedBudget: number;
128
+ readonly bounds: PolicyBounds;
129
+ readonly ts: string;
130
+ }
131
+
132
+ /**
133
+ * PressureV2 — a canonical pressure observation for a session at an effective
134
+ * sequence point.
135
+ */
136
+ export interface PressureV2 {
137
+ readonly schema: typeof PRESSURE_SCHEMA_V2;
138
+ readonly level: PressureLevel;
139
+ readonly sessionId: string;
140
+ readonly effectiveSeq: number;
141
+ readonly ts: string;
142
+ }
143
+
144
+ /** Shadow metrics — counts only, never prompt bytes or free-text. */
145
+ export interface ShadowMetrics {
146
+ /** Decisions evaluated in this shadow run. */
147
+ readonly evaluated: number;
148
+ /** Decisions whose budget was clamped at either bound. */
149
+ readonly clamped: number;
150
+ /** Inputs rejected (unknown pressure / bad bounds). */
151
+ readonly rejected: number;
152
+ /**
153
+ * Live mutations performed by the shadow engine. Structurally ALWAYS 0 —
154
+ * the shadow has no writer capability — and asserted as 0 by the sprint's
155
+ * acceptance contract.
156
+ */
157
+ readonly liveMutations: number;
158
+ }
159
+
160
+ /** The result of a shadow evaluation: decisions + metrics ONLY. */
161
+ export interface ShadowResult {
162
+ readonly decisions: ReadonlyArray<PolicyDecisionV1>;
163
+ readonly rejections: ReadonlyArray<ShadowRejection>;
164
+ readonly metrics: ShadowMetrics;
165
+ /**
166
+ * The digest of the canonical prompt as observed on entry. The shadow engine
167
+ * re-computes this on exit and the two MUST be equal (POL-SHADOW-002).
168
+ */
169
+ readonly promptDigest: string;
170
+ }
171
+
172
+ /** A rejected shadow input, reduced to its code. */
173
+ export interface ShadowRejection {
174
+ readonly decisionId: string;
175
+ readonly code: string;
176
+ }
177
+
178
+ /** Conformance IDs POL-001..POL-025 for the 25 numbered policy rows. */
179
+ export const POLICY_CONFORMANCE_IDS: readonly string[] = Array.from(
180
+ { length: 25 },
181
+ (_v, i) => `POL-${String(i + 1).padStart(3, "0")}`,
182
+ );
183
+
184
+ /** Conformance IDs M7-001..M7-015 for the 15 numbered migration rows. */
185
+ export const M7_CONFORMANCE_IDS: readonly string[] = Array.from(
186
+ { length: 15 },
187
+ (_v, i) => `M7-${String(i + 1).padStart(3, "0")}`,
188
+ );
189
+
190
+ /** Named conformance fixtures for the sprint's headline assertions. */
191
+ export const POLICY_NAMED_FIXTURES = [
192
+ "POL-CLAMP-001",
193
+ "POL-SHADOW-002",
194
+ "M7-PRESSURE-003",
195
+ ] as const;