@derwinjs/db 0.7.0 → 0.9.0
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/auto-promotion-evaluator.d.ts +63 -0
- package/dist/auto-promotion-evaluator.d.ts.map +1 -0
- package/dist/auto-promotion-evaluator.js +195 -0
- package/dist/auto-promotion-evaluator.js.map +1 -0
- package/dist/budget-gate.d.ts +43 -0
- package/dist/budget-gate.d.ts.map +1 -0
- package/dist/budget-gate.js +110 -0
- package/dist/budget-gate.js.map +1 -0
- package/dist/classification-override-store.d.ts +24 -0
- package/dist/classification-override-store.d.ts.map +1 -0
- package/dist/classification-override-store.js +131 -0
- package/dist/classification-override-store.js.map +1 -0
- package/dist/freeze-window-evaluator.d.ts +62 -0
- package/dist/freeze-window-evaluator.d.ts.map +1 -0
- package/dist/freeze-window-evaluator.js +236 -0
- package/dist/freeze-window-evaluator.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/kill-switch-evaluator.d.ts +68 -0
- package/dist/kill-switch-evaluator.d.ts.map +1 -0
- package/dist/kill-switch-evaluator.js +389 -0
- package/dist/kill-switch-evaluator.js.map +1 -0
- package/dist/path-tier-resolver.d.ts +47 -0
- package/dist/path-tier-resolver.d.ts.map +1 -0
- package/dist/path-tier-resolver.js +177 -0
- package/dist/path-tier-resolver.js.map +1 -0
- package/dist/project-mode-store.d.ts +28 -0
- package/dist/project-mode-store.d.ts.map +1 -0
- package/dist/project-mode-store.js +126 -0
- package/dist/project-mode-store.js.map +1 -0
- package/dist/trust-threshold-config-store.d.ts +20 -0
- package/dist/trust-threshold-config-store.d.ts.map +1 -0
- package/dist/trust-threshold-config-store.js +88 -0
- package/dist/trust-threshold-config-store.js.map +1 -0
- package/package.json +3 -3
- package/prisma/migrations/20260507120000_sprint8_policy_governance/migration.sql +58 -0
- package/prisma/migrations/20260507120100_sprint8_phase2_thresholds_and_freeze/migration.sql +33 -0
- package/prisma/migrations/20260507120200_sprint8_phase3_budget_cap/migration.sql +21 -0
- package/prisma/migrations/20260507120300_sprint8_phase4_kill_switches/migration.sql +74 -0
- package/prisma/schema.prisma +183 -11
- package/prisma-client/edge.js +96 -19
- package/prisma-client/index-browser.js +93 -16
- package/prisma-client/index.d.ts +10997 -3426
- package/prisma-client/index.js +96 -19
- package/prisma-client/package.json +1 -1
- package/prisma-client/schema.prisma +183 -11
- package/prisma-client/wasm.js +93 -16
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaAutoPromotionEvaluator — Prisma-backed implementation of the
|
|
3
|
+
* SDK AutoPromotionEvaluator contract introduced by QAP-095.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 9 Phase 3 (Policy + UI). Pure-read evaluator. Reads project
|
|
6
|
+
* mode-change boundary + recent telemetry and reports a 5-gate eligibility
|
|
7
|
+
* snapshot (see product brief §11.2 promotion criteria). NEVER mutates
|
|
8
|
+
* state — promotion itself is a separate ProjectModeStore.setMode() call
|
|
9
|
+
* the UI / cron makes once an operator confirms.
|
|
10
|
+
*
|
|
11
|
+
* # Window semantics
|
|
12
|
+
*
|
|
13
|
+
* t0 = now (config-injectable for tests)
|
|
14
|
+
* lookbackStart = t0 - PROMOTION_LOOKBACK_DAYS * 1d
|
|
15
|
+
*
|
|
16
|
+
* attempts/regression queries scope to attemptedAt >= lookbackStart.
|
|
17
|
+
* escalations queries scope to QATicket.updatedAt >= lookbackStart
|
|
18
|
+
* AND finalBucket = ESCALATION (the bucket assignment lands on
|
|
19
|
+
* updatedAt — see QATicket schema header).
|
|
20
|
+
*
|
|
21
|
+
* # Trust aggregation
|
|
22
|
+
*
|
|
23
|
+
* trustPercent is a SAMPLE-SIZE-WEIGHTED average. Each ClassificationTrust
|
|
24
|
+
* row contributes `trustPercent * attemptsLast30d` to the numerator and
|
|
25
|
+
* `attemptsLast30d` to the denominator. A row with `attemptsLast30d=0`
|
|
26
|
+
* contributes nothing — a brand-new tuple with no observations doesn't
|
|
27
|
+
* dilute the average. If NO rows have any observations, the gate's
|
|
28
|
+
* current value is 0 (which fails the >= 70 threshold by design).
|
|
29
|
+
*
|
|
30
|
+
* # Tenant isolation
|
|
31
|
+
*
|
|
32
|
+
* Every query scopes by projectId. The Project row read at the top
|
|
33
|
+
* (modeChangedAt) doubles as the existence check — `project_not_found`
|
|
34
|
+
* is thrown before any per-window aggregation runs, so we never spend
|
|
35
|
+
* round trips for a missing project.
|
|
36
|
+
*/
|
|
37
|
+
import type { PrismaClient } from './prisma.js';
|
|
38
|
+
import { type AutoPromotionEvaluator } from '@derwinjs/sdk';
|
|
39
|
+
export interface PrismaAutoPromotionEvaluatorConfig {
|
|
40
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
41
|
+
prisma: PrismaClient;
|
|
42
|
+
/**
|
|
43
|
+
* Optional clock injection for deterministic window-boundary tests.
|
|
44
|
+
* Tests pass a fixed Date; production callers omit (defaults to
|
|
45
|
+
* `new Date()`).
|
|
46
|
+
*/
|
|
47
|
+
now?: () => Date;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Public default thresholds — exported so the policy editor UI can render
|
|
51
|
+
* "needs >= 30 days" copy without round-tripping through the evaluator.
|
|
52
|
+
* Mirrors the constants above; if a caller-tunable variant ever lands,
|
|
53
|
+
* the constants stay as the wired defaults.
|
|
54
|
+
*/
|
|
55
|
+
export declare const DEFAULT_PROMOTION_THRESHOLDS: {
|
|
56
|
+
readonly daysOperating: 30;
|
|
57
|
+
readonly attempts: 50;
|
|
58
|
+
readonly trustPercent: 70;
|
|
59
|
+
readonly regressionPercent: 5;
|
|
60
|
+
readonly escalations: 0;
|
|
61
|
+
};
|
|
62
|
+
export declare function createPrismaAutoPromotionEvaluator(config: PrismaAutoPromotionEvaluatorConfig): AutoPromotionEvaluator;
|
|
63
|
+
//# sourceMappingURL=auto-promotion-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-promotion-evaluator.d.ts","sourceRoot":"","sources":["../src/auto-promotion-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAEL,KAAK,sBAAsB,EAG5B,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,kCAAkC;IACjD,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;IACrB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAYD;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B;;;;;;CAM/B,CAAC;AA6BX,wBAAgB,kCAAkC,CAChD,MAAM,EAAE,kCAAkC,GACzC,sBAAsB,CAuIxB"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaAutoPromotionEvaluator — Prisma-backed implementation of the
|
|
3
|
+
* SDK AutoPromotionEvaluator contract introduced by QAP-095.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 9 Phase 3 (Policy + UI). Pure-read evaluator. Reads project
|
|
6
|
+
* mode-change boundary + recent telemetry and reports a 5-gate eligibility
|
|
7
|
+
* snapshot (see product brief §11.2 promotion criteria). NEVER mutates
|
|
8
|
+
* state — promotion itself is a separate ProjectModeStore.setMode() call
|
|
9
|
+
* the UI / cron makes once an operator confirms.
|
|
10
|
+
*
|
|
11
|
+
* # Window semantics
|
|
12
|
+
*
|
|
13
|
+
* t0 = now (config-injectable for tests)
|
|
14
|
+
* lookbackStart = t0 - PROMOTION_LOOKBACK_DAYS * 1d
|
|
15
|
+
*
|
|
16
|
+
* attempts/regression queries scope to attemptedAt >= lookbackStart.
|
|
17
|
+
* escalations queries scope to QATicket.updatedAt >= lookbackStart
|
|
18
|
+
* AND finalBucket = ESCALATION (the bucket assignment lands on
|
|
19
|
+
* updatedAt — see QATicket schema header).
|
|
20
|
+
*
|
|
21
|
+
* # Trust aggregation
|
|
22
|
+
*
|
|
23
|
+
* trustPercent is a SAMPLE-SIZE-WEIGHTED average. Each ClassificationTrust
|
|
24
|
+
* row contributes `trustPercent * attemptsLast30d` to the numerator and
|
|
25
|
+
* `attemptsLast30d` to the denominator. A row with `attemptsLast30d=0`
|
|
26
|
+
* contributes nothing — a brand-new tuple with no observations doesn't
|
|
27
|
+
* dilute the average. If NO rows have any observations, the gate's
|
|
28
|
+
* current value is 0 (which fails the >= 70 threshold by design).
|
|
29
|
+
*
|
|
30
|
+
* # Tenant isolation
|
|
31
|
+
*
|
|
32
|
+
* Every query scopes by projectId. The Project row read at the top
|
|
33
|
+
* (modeChangedAt) doubles as the existence check — `project_not_found`
|
|
34
|
+
* is thrown before any per-window aggregation runs, so we never spend
|
|
35
|
+
* round trips for a missing project.
|
|
36
|
+
*/
|
|
37
|
+
import { AutoPromotionEvaluatorError, } from '@derwinjs/sdk';
|
|
38
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
39
|
+
const PROMOTION_DAYS_OPERATING_MIN = 30;
|
|
40
|
+
const PROMOTION_ATTEMPTS_MIN = 50;
|
|
41
|
+
const PROMOTION_TRUST_MIN = 70;
|
|
42
|
+
const PROMOTION_REGRESSION_MAX = 5;
|
|
43
|
+
const PROMOTION_ESCALATIONS_MAX = 0;
|
|
44
|
+
const PROMOTION_LOOKBACK_DAYS = 30;
|
|
45
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
46
|
+
/**
|
|
47
|
+
* Public default thresholds — exported so the policy editor UI can render
|
|
48
|
+
* "needs >= 30 days" copy without round-tripping through the evaluator.
|
|
49
|
+
* Mirrors the constants above; if a caller-tunable variant ever lands,
|
|
50
|
+
* the constants stay as the wired defaults.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_PROMOTION_THRESHOLDS = {
|
|
53
|
+
daysOperating: PROMOTION_DAYS_OPERATING_MIN,
|
|
54
|
+
attempts: PROMOTION_ATTEMPTS_MIN,
|
|
55
|
+
trustPercent: PROMOTION_TRUST_MIN,
|
|
56
|
+
regressionPercent: PROMOTION_REGRESSION_MAX,
|
|
57
|
+
escalations: PROMOTION_ESCALATIONS_MAX,
|
|
58
|
+
};
|
|
59
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
60
|
+
const MERGED_DISPATCH_STATUSES = ['AUTO_MERGED', 'HUMAN_MERGED'];
|
|
61
|
+
/**
|
|
62
|
+
* Sample-size-weighted average of trustPercent across rows.
|
|
63
|
+
* Returns `Math.round(sum(trustPercent * total) / sum(total))`.
|
|
64
|
+
* Returns 0 when there are no rows OR no observations across rows.
|
|
65
|
+
*/
|
|
66
|
+
function weightedTrustAverage(rows) {
|
|
67
|
+
let weightedSum = 0;
|
|
68
|
+
let totalSum = 0;
|
|
69
|
+
for (const row of rows) {
|
|
70
|
+
weightedSum += row.trustPercent * row.attemptsLast30d;
|
|
71
|
+
totalSum += row.attemptsLast30d;
|
|
72
|
+
}
|
|
73
|
+
if (totalSum === 0)
|
|
74
|
+
return 0;
|
|
75
|
+
return Math.round(weightedSum / totalSum);
|
|
76
|
+
}
|
|
77
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
78
|
+
export function createPrismaAutoPromotionEvaluator(config) {
|
|
79
|
+
const { prisma } = config;
|
|
80
|
+
const now = config.now ?? (() => new Date());
|
|
81
|
+
return {
|
|
82
|
+
async evaluate(input) {
|
|
83
|
+
if (typeof input.projectId !== 'string' || input.projectId.length === 0) {
|
|
84
|
+
throw new AutoPromotionEvaluatorError('invalid_input', 'AutoPromotionEvaluator: projectId is required');
|
|
85
|
+
}
|
|
86
|
+
const at = now();
|
|
87
|
+
const lookbackStart = new Date(at.getTime() - PROMOTION_LOOKBACK_DAYS * MS_PER_DAY);
|
|
88
|
+
// Project existence check + modeChangedAt for daysOperating gate.
|
|
89
|
+
// This double-duty read also gives us tenant isolation — a missing /
|
|
90
|
+
// wrong-project id throws before we run any per-window aggregation.
|
|
91
|
+
const project = await prisma.project.findUnique({
|
|
92
|
+
where: { id: input.projectId },
|
|
93
|
+
select: { id: true, modeChangedAt: true },
|
|
94
|
+
});
|
|
95
|
+
if (project === null) {
|
|
96
|
+
throw new AutoPromotionEvaluatorError('project_not_found', `AutoPromotionEvaluator: project ${input.projectId} not found`);
|
|
97
|
+
}
|
|
98
|
+
// Gate 1 — daysOperating (floor((now - modeChangedAt) / 1d))
|
|
99
|
+
const daysOperating = Math.floor((at.getTime() - project.modeChangedAt.getTime()) / MS_PER_DAY);
|
|
100
|
+
// Gate 2 — attempts in last 30d (AUTO_MERGED ∪ HUMAN_MERGED)
|
|
101
|
+
const attempts = await prisma.qAFixAttempt.count({
|
|
102
|
+
where: {
|
|
103
|
+
projectId: input.projectId,
|
|
104
|
+
dispatchStatus: { in: [...MERGED_DISPATCH_STATUSES] },
|
|
105
|
+
attemptedAt: { gte: lookbackStart },
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
// Gate 3 — trustPercent (sample-size-weighted average across all
|
|
109
|
+
// (classification, surface) tuples for this project).
|
|
110
|
+
const trustRows = await prisma.classificationTrust.findMany({
|
|
111
|
+
where: { projectId: input.projectId },
|
|
112
|
+
select: { trustPercent: true, attemptsLast30d: true },
|
|
113
|
+
});
|
|
114
|
+
const trustPercent = weightedTrustAverage(trustRows);
|
|
115
|
+
// Gate 4 — regressionPercent (regressed / total in last 30d, * 100).
|
|
116
|
+
// totalRecent counts ALL dispatchStatus values within the window so
|
|
117
|
+
// the denominator captures every post-author attempt, not just merged
|
|
118
|
+
// ones. This matches the operator-facing definition of "recent fix
|
|
119
|
+
// activity" used elsewhere in the policy editor.
|
|
120
|
+
const totalRecent = await prisma.qAFixAttempt.count({
|
|
121
|
+
where: {
|
|
122
|
+
projectId: input.projectId,
|
|
123
|
+
attemptedAt: { gte: lookbackStart },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const regressed = await prisma.qAFixAttempt.count({
|
|
127
|
+
where: {
|
|
128
|
+
projectId: input.projectId,
|
|
129
|
+
regressionDetected: true,
|
|
130
|
+
attemptedAt: { gte: lookbackStart },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const regressionPercent = totalRecent === 0 ? 0 : Math.round((regressed / totalRecent) * 100);
|
|
134
|
+
// Gate 5 — escalations (QATicket.finalBucket=ESCALATION in last 30d).
|
|
135
|
+
const escalations = await prisma.qATicket.count({
|
|
136
|
+
where: {
|
|
137
|
+
projectId: input.projectId,
|
|
138
|
+
finalBucket: 'ESCALATION',
|
|
139
|
+
updatedAt: { gte: lookbackStart },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
// Build the 5 gate results in a stable order. UI renders them as a
|
|
143
|
+
// table in this order; reordering would break operator muscle memory.
|
|
144
|
+
const gates = [
|
|
145
|
+
{
|
|
146
|
+
name: 'daysOperating',
|
|
147
|
+
pass: daysOperating >= PROMOTION_DAYS_OPERATING_MIN,
|
|
148
|
+
current: daysOperating,
|
|
149
|
+
threshold: PROMOTION_DAYS_OPERATING_MIN,
|
|
150
|
+
comparator: 'gte',
|
|
151
|
+
label: 'Days operating in current mode',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'attempts',
|
|
155
|
+
pass: attempts >= PROMOTION_ATTEMPTS_MIN,
|
|
156
|
+
current: attempts,
|
|
157
|
+
threshold: PROMOTION_ATTEMPTS_MIN,
|
|
158
|
+
comparator: 'gte',
|
|
159
|
+
label: 'Merged attempts (last 30 days)',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'trustPercent',
|
|
163
|
+
pass: trustPercent >= PROMOTION_TRUST_MIN,
|
|
164
|
+
current: trustPercent,
|
|
165
|
+
threshold: PROMOTION_TRUST_MIN,
|
|
166
|
+
comparator: 'gte',
|
|
167
|
+
label: 'Weighted trust percent',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'regressionPercent',
|
|
171
|
+
pass: regressionPercent <= PROMOTION_REGRESSION_MAX,
|
|
172
|
+
current: regressionPercent,
|
|
173
|
+
threshold: PROMOTION_REGRESSION_MAX,
|
|
174
|
+
comparator: 'lte',
|
|
175
|
+
label: 'Regression rate (last 30 days)',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'escalations',
|
|
179
|
+
pass: escalations === PROMOTION_ESCALATIONS_MAX,
|
|
180
|
+
current: escalations,
|
|
181
|
+
threshold: PROMOTION_ESCALATIONS_MAX,
|
|
182
|
+
comparator: 'eq',
|
|
183
|
+
label: 'Escalations (last 30 days)',
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const eligible = gates.every((gate) => gate.pass);
|
|
187
|
+
return {
|
|
188
|
+
eligible,
|
|
189
|
+
gates,
|
|
190
|
+
computedAt: at,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=auto-promotion-evaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-promotion-evaluator.js","sourceRoot":"","sources":["../src/auto-promotion-evaluator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaBudgetGate — Prisma-backed implementation of the SDK
|
|
3
|
+
* BudgetGate contract introduced by QAP-084.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 Phase 3 (Policy Governance). Per-project monthly auto-fix budget
|
|
6
|
+
* cap with soft-warn + hard-stop thresholds. The dispatcher consults
|
|
7
|
+
* `isHardStopped()` BEFORE running runFixCycle and refuses to dispatch
|
|
8
|
+
* when month-to-date spend has hit the cap.
|
|
9
|
+
*
|
|
10
|
+
* # Spend window
|
|
11
|
+
*
|
|
12
|
+
* Month-to-date is the calendar month in UTC: spend SUM is restricted to
|
|
13
|
+
* SpendLedger entries with `occurredAt >= startOfMonth(UTC)`. The window
|
|
14
|
+
* resets at 00:00 UTC on the 1st of each month — this matches operator
|
|
15
|
+
* mental model (`this month's budget`, not a rolling 30-day window).
|
|
16
|
+
*
|
|
17
|
+
* # Currency unit
|
|
18
|
+
*
|
|
19
|
+
* SpendLedger.costCents is INTEGER cents (legacy schema, preserved for
|
|
20
|
+
* back-compat with the audit trail and the per-attempt costCents column).
|
|
21
|
+
* The BudgetGate contract surfaces USD floats — we divide by 100 in the
|
|
22
|
+
* factory rather than mutating the schema. New consumers wire BudgetGate
|
|
23
|
+
* and never touch cents directly.
|
|
24
|
+
*
|
|
25
|
+
* # Tenant isolation
|
|
26
|
+
*
|
|
27
|
+
* Every method scopes by projectId. Pattern D — wrong-project / unknown-
|
|
28
|
+
* project lookups return state=`NO_CAP` (defense-in-depth: a missing
|
|
29
|
+
* Project row should not look different from one with no cap configured).
|
|
30
|
+
*/
|
|
31
|
+
import type { PrismaClient } from './prisma.js';
|
|
32
|
+
import { type BudgetGate } from '@derwinjs/sdk';
|
|
33
|
+
export interface PrismaBudgetGateConfig {
|
|
34
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
35
|
+
prisma: PrismaClient;
|
|
36
|
+
/**
|
|
37
|
+
* Optional clock injection for deterministic month-boundary tests. Tests
|
|
38
|
+
* pass a fixed Date; production callers omit (defaults to `new Date()`).
|
|
39
|
+
*/
|
|
40
|
+
now?: () => Date;
|
|
41
|
+
}
|
|
42
|
+
export declare function createPrismaBudgetGate(config: PrismaBudgetGateConfig): BudgetGate;
|
|
43
|
+
//# sourceMappingURL=budget-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget-gate.d.ts","sourceRoot":"","sources":["../src/budget-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAmB,KAAK,UAAU,EAAqB,MAAM,eAAe,CAAC;AAIpF,MAAM,WAAW,sBAAsB;IACrC,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;IACrB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AA8BD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,UAAU,CA8DjF"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaBudgetGate — Prisma-backed implementation of the SDK
|
|
3
|
+
* BudgetGate contract introduced by QAP-084.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 Phase 3 (Policy Governance). Per-project monthly auto-fix budget
|
|
6
|
+
* cap with soft-warn + hard-stop thresholds. The dispatcher consults
|
|
7
|
+
* `isHardStopped()` BEFORE running runFixCycle and refuses to dispatch
|
|
8
|
+
* when month-to-date spend has hit the cap.
|
|
9
|
+
*
|
|
10
|
+
* # Spend window
|
|
11
|
+
*
|
|
12
|
+
* Month-to-date is the calendar month in UTC: spend SUM is restricted to
|
|
13
|
+
* SpendLedger entries with `occurredAt >= startOfMonth(UTC)`. The window
|
|
14
|
+
* resets at 00:00 UTC on the 1st of each month — this matches operator
|
|
15
|
+
* mental model (`this month's budget`, not a rolling 30-day window).
|
|
16
|
+
*
|
|
17
|
+
* # Currency unit
|
|
18
|
+
*
|
|
19
|
+
* SpendLedger.costCents is INTEGER cents (legacy schema, preserved for
|
|
20
|
+
* back-compat with the audit trail and the per-attempt costCents column).
|
|
21
|
+
* The BudgetGate contract surfaces USD floats — we divide by 100 in the
|
|
22
|
+
* factory rather than mutating the schema. New consumers wire BudgetGate
|
|
23
|
+
* and never touch cents directly.
|
|
24
|
+
*
|
|
25
|
+
* # Tenant isolation
|
|
26
|
+
*
|
|
27
|
+
* Every method scopes by projectId. Pattern D — wrong-project / unknown-
|
|
28
|
+
* project lookups return state=`NO_CAP` (defense-in-depth: a missing
|
|
29
|
+
* Project row should not look different from one with no cap configured).
|
|
30
|
+
*/
|
|
31
|
+
import { BudgetGateError } from '@derwinjs/sdk';
|
|
32
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Compute the start of the calendar month in UTC for the given instant.
|
|
35
|
+
* `2026-05-07T12:34:56Z` → `2026-05-01T00:00:00.000Z`. The aggregator query
|
|
36
|
+
* uses `occurredAt >= startOfMonth` so the boundary is inclusive.
|
|
37
|
+
*/
|
|
38
|
+
function startOfMonthUtc(at) {
|
|
39
|
+
return new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), 1, 0, 0, 0, 0));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Decide the gate state from utilization. Mirrors the BudgetGate contract's
|
|
43
|
+
* documented thresholds exactly.
|
|
44
|
+
*/
|
|
45
|
+
function deriveState(monthlyBudgetUsd, utilizationPercent, softWarnPct) {
|
|
46
|
+
if (monthlyBudgetUsd === null || utilizationPercent === null)
|
|
47
|
+
return 'NO_CAP';
|
|
48
|
+
if (utilizationPercent >= 1.0)
|
|
49
|
+
return 'HARD_STOP';
|
|
50
|
+
if (utilizationPercent >= softWarnPct)
|
|
51
|
+
return 'SOFT_WARN';
|
|
52
|
+
return 'OK';
|
|
53
|
+
}
|
|
54
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
55
|
+
export function createPrismaBudgetGate(config) {
|
|
56
|
+
const { prisma } = config;
|
|
57
|
+
const now = config.now ?? (() => new Date());
|
|
58
|
+
async function computeStatus(projectId) {
|
|
59
|
+
if (typeof projectId !== 'string' || projectId.length === 0) {
|
|
60
|
+
throw new BudgetGateError('invalid_input', 'BudgetGate: projectId is required');
|
|
61
|
+
}
|
|
62
|
+
// 1. Fetch the project's cap + soft-warn pct. Pattern D: unknown
|
|
63
|
+
// project → NO_CAP, no throw. Defense-in-depth so a tenant
|
|
64
|
+
// enumeration probe sees the same shape as a real cap-less project.
|
|
65
|
+
const project = await prisma.project.findUnique({
|
|
66
|
+
where: { id: projectId },
|
|
67
|
+
select: { monthlyBudgetUsd: true, budgetSoftWarnPct: true },
|
|
68
|
+
});
|
|
69
|
+
if (project === null) {
|
|
70
|
+
return {
|
|
71
|
+
monthToDateSpendUsd: 0,
|
|
72
|
+
monthlyBudgetUsd: null,
|
|
73
|
+
utilizationPercent: null,
|
|
74
|
+
state: 'NO_CAP',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// 2. Sum the SpendLedger entries for the current calendar month.
|
|
78
|
+
// SpendLedger.costCents is INT cents — we divide by 100 to surface USD.
|
|
79
|
+
const monthStart = startOfMonthUtc(now());
|
|
80
|
+
const aggregate = await prisma.spendLedger.aggregate({
|
|
81
|
+
where: { projectId, occurredAt: { gte: monthStart } },
|
|
82
|
+
_sum: { costCents: true },
|
|
83
|
+
});
|
|
84
|
+
const totalCents = aggregate._sum.costCents ?? 0;
|
|
85
|
+
const monthToDateSpendUsd = totalCents / 100;
|
|
86
|
+
// 3. Compute utilization + state.
|
|
87
|
+
const monthlyBudgetUsd = project.monthlyBudgetUsd ?? null;
|
|
88
|
+
const utilizationPercent = monthlyBudgetUsd === null || monthlyBudgetUsd === 0
|
|
89
|
+
? null
|
|
90
|
+
: monthToDateSpendUsd / monthlyBudgetUsd;
|
|
91
|
+
const softWarnPct = project.budgetSoftWarnPct;
|
|
92
|
+
const state = deriveState(monthlyBudgetUsd, utilizationPercent, softWarnPct);
|
|
93
|
+
return {
|
|
94
|
+
monthToDateSpendUsd,
|
|
95
|
+
monthlyBudgetUsd,
|
|
96
|
+
utilizationPercent,
|
|
97
|
+
state,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
async getStatus(input) {
|
|
102
|
+
return computeStatus(input.projectId);
|
|
103
|
+
},
|
|
104
|
+
async isHardStopped(input) {
|
|
105
|
+
const status = await computeStatus(input.projectId);
|
|
106
|
+
return status.state === 'HARD_STOP';
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=budget-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget-gate.js","sourceRoot":"","sources":["../src/budget-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaClassificationOverrideStore — Prisma-backed implementation of
|
|
3
|
+
* the SDK ClassificationOverrideStore contract introduced by QAP-081.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 (Policy Governance, Phase 1). Operator-defined overrides keyed
|
|
6
|
+
* by (classification, surface). Three modes:
|
|
7
|
+
*
|
|
8
|
+
* - BLOCK_AUTO_FIX — never auto-fix this (classification, surface)
|
|
9
|
+
* - FORCE_ESCALATION — route directly to ESCALATION bucket
|
|
10
|
+
* - DOWNGRADE_TIER — replace the resolved RiskTier with `downgradeTier`
|
|
11
|
+
*
|
|
12
|
+
* Tenant isolation: every method scopes by projectId. findOverride
|
|
13
|
+
* resolution: exact-surface match wins over wildcard (surface=null).
|
|
14
|
+
* Cross-project ids return null on findOverride / throw `not_found` on
|
|
15
|
+
* deleteOverride. Pattern D from docs/testing.md applies.
|
|
16
|
+
*/
|
|
17
|
+
import type { PrismaClient } from './prisma.js';
|
|
18
|
+
import { type ClassificationOverrideStore } from '@derwinjs/sdk';
|
|
19
|
+
export interface PrismaClassificationOverrideStoreConfig {
|
|
20
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
21
|
+
prisma: PrismaClient;
|
|
22
|
+
}
|
|
23
|
+
export declare function createPrismaClassificationOverrideStore(config: PrismaClassificationOverrideStoreConfig): ClassificationOverrideStore;
|
|
24
|
+
//# sourceMappingURL=classification-override-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classification-override-store.d.ts","sourceRoot":"","sources":["../src/classification-override-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAKL,KAAK,2BAA2B,EAEjC,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,uCAAuC;IACtD,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAyFD,wBAAgB,uCAAuC,CACrD,MAAM,EAAE,uCAAuC,GAC9C,2BAA2B,CAyF7B"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaClassificationOverrideStore — Prisma-backed implementation of
|
|
3
|
+
* the SDK ClassificationOverrideStore contract introduced by QAP-081.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 (Policy Governance, Phase 1). Operator-defined overrides keyed
|
|
6
|
+
* by (classification, surface). Three modes:
|
|
7
|
+
*
|
|
8
|
+
* - BLOCK_AUTO_FIX — never auto-fix this (classification, surface)
|
|
9
|
+
* - FORCE_ESCALATION — route directly to ESCALATION bucket
|
|
10
|
+
* - DOWNGRADE_TIER — replace the resolved RiskTier with `downgradeTier`
|
|
11
|
+
*
|
|
12
|
+
* Tenant isolation: every method scopes by projectId. findOverride
|
|
13
|
+
* resolution: exact-surface match wins over wildcard (surface=null).
|
|
14
|
+
* Cross-project ids return null on findOverride / throw `not_found` on
|
|
15
|
+
* deleteOverride. Pattern D from docs/testing.md applies.
|
|
16
|
+
*/
|
|
17
|
+
import { ClassificationOverrideStoreError, OverrideModeValues, RiskTierValues, } from '@derwinjs/sdk';
|
|
18
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
19
|
+
function validateCreateInput(input) {
|
|
20
|
+
if (typeof input.classification !== 'string' || input.classification.length === 0) {
|
|
21
|
+
throw new ClassificationOverrideStoreError('invalid_input', 'ClassificationOverrideStore: classification is required');
|
|
22
|
+
}
|
|
23
|
+
if (!OverrideModeValues.includes(input.overrideMode)) {
|
|
24
|
+
throw new ClassificationOverrideStoreError('invalid_input', `ClassificationOverrideStore: overrideMode must be one of ${OverrideModeValues.join(', ')}`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof input.reason !== 'string' || input.reason.trim().length === 0) {
|
|
27
|
+
throw new ClassificationOverrideStoreError('invalid_input', 'ClassificationOverrideStore: reason is required');
|
|
28
|
+
}
|
|
29
|
+
if (typeof input.createdBy !== 'string' || input.createdBy.trim().length === 0) {
|
|
30
|
+
throw new ClassificationOverrideStoreError('invalid_input', 'ClassificationOverrideStore: createdBy is required');
|
|
31
|
+
}
|
|
32
|
+
if (input.overrideMode === 'DOWNGRADE_TIER') {
|
|
33
|
+
if (input.downgradeTier === undefined || input.downgradeTier === null) {
|
|
34
|
+
throw new ClassificationOverrideStoreError('invalid_input', 'ClassificationOverrideStore: downgradeTier is required when overrideMode === DOWNGRADE_TIER');
|
|
35
|
+
}
|
|
36
|
+
if (!RiskTierValues.includes(input.downgradeTier)) {
|
|
37
|
+
throw new ClassificationOverrideStoreError('invalid_input', `ClassificationOverrideStore: downgradeTier must be one of ${RiskTierValues.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// For non-DOWNGRADE_TIER modes, downgradeTier MUST be null/undefined.
|
|
42
|
+
if (input.downgradeTier !== undefined && input.downgradeTier !== null) {
|
|
43
|
+
throw new ClassificationOverrideStoreError('invalid_input', 'ClassificationOverrideStore: downgradeTier must be null unless overrideMode === DOWNGRADE_TIER');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function mapOverride(row) {
|
|
48
|
+
return {
|
|
49
|
+
id: row.id,
|
|
50
|
+
projectId: row.projectId,
|
|
51
|
+
classification: row.classification,
|
|
52
|
+
surface: row.surface,
|
|
53
|
+
overrideMode: row.overrideMode,
|
|
54
|
+
downgradeTier: row.downgradeTier,
|
|
55
|
+
reason: row.reason,
|
|
56
|
+
createdBy: row.createdBy,
|
|
57
|
+
createdAt: row.createdAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
61
|
+
export function createPrismaClassificationOverrideStore(config) {
|
|
62
|
+
const { prisma } = config;
|
|
63
|
+
return {
|
|
64
|
+
async findOverride(input) {
|
|
65
|
+
// Resolution: exact-surface match wins over wildcard (surface=null).
|
|
66
|
+
// Implemented as TWO queries (exact first, then wildcard) so the
|
|
67
|
+
// intent is unambiguous. Both findFirst calls scope by projectId for
|
|
68
|
+
// tenant isolation.
|
|
69
|
+
const exact = await prisma.classificationOverride.findFirst({
|
|
70
|
+
where: {
|
|
71
|
+
projectId: input.projectId,
|
|
72
|
+
classification: input.classification,
|
|
73
|
+
surface: input.surface,
|
|
74
|
+
},
|
|
75
|
+
orderBy: { createdAt: 'desc' },
|
|
76
|
+
});
|
|
77
|
+
if (exact !== null)
|
|
78
|
+
return mapOverride(exact);
|
|
79
|
+
const wildcard = await prisma.classificationOverride.findFirst({
|
|
80
|
+
where: {
|
|
81
|
+
projectId: input.projectId,
|
|
82
|
+
classification: input.classification,
|
|
83
|
+
surface: null,
|
|
84
|
+
},
|
|
85
|
+
orderBy: { createdAt: 'desc' },
|
|
86
|
+
});
|
|
87
|
+
if (wildcard !== null)
|
|
88
|
+
return mapOverride(wildcard);
|
|
89
|
+
return null;
|
|
90
|
+
},
|
|
91
|
+
async listOverrides(input) {
|
|
92
|
+
const rows = await prisma.classificationOverride.findMany({
|
|
93
|
+
where: { projectId: input.projectId },
|
|
94
|
+
orderBy: { createdAt: 'desc' },
|
|
95
|
+
});
|
|
96
|
+
return rows.map(mapOverride);
|
|
97
|
+
},
|
|
98
|
+
async createOverride(input) {
|
|
99
|
+
validateCreateInput(input);
|
|
100
|
+
const row = await prisma.classificationOverride.create({
|
|
101
|
+
data: {
|
|
102
|
+
projectId: input.projectId,
|
|
103
|
+
classification: input.classification,
|
|
104
|
+
surface: input.surface,
|
|
105
|
+
overrideMode: input.overrideMode,
|
|
106
|
+
downgradeTier: input.overrideMode === 'DOWNGRADE_TIER'
|
|
107
|
+
? (input.downgradeTier ?? null)
|
|
108
|
+
: null,
|
|
109
|
+
reason: input.reason,
|
|
110
|
+
createdBy: input.createdBy,
|
|
111
|
+
},
|
|
112
|
+
select: { id: true },
|
|
113
|
+
});
|
|
114
|
+
return { id: row.id };
|
|
115
|
+
},
|
|
116
|
+
async deleteOverride(input) {
|
|
117
|
+
// Tenant-check first via findFirst — same defense-in-depth pattern as
|
|
118
|
+
// QAPatternStore.updatePatternStatus. Surfacing not_found early gives
|
|
119
|
+
// callers a typed error rather than a silent no-op.
|
|
120
|
+
const existing = await prisma.classificationOverride.findFirst({
|
|
121
|
+
where: { id: input.overrideId, projectId: input.projectId },
|
|
122
|
+
select: { id: true },
|
|
123
|
+
});
|
|
124
|
+
if (existing === null) {
|
|
125
|
+
throw new ClassificationOverrideStoreError('not_found', `ClassificationOverride ${input.overrideId} not found in project ${input.projectId}`);
|
|
126
|
+
}
|
|
127
|
+
await prisma.classificationOverride.delete({ where: { id: input.overrideId } });
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=classification-override-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classification-override-store.js","sourceRoot":"","sources":["../src/classification-override-store.ts"],"names":[],"mappings":"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"}
|