@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,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaKillSwitchEvaluator — Prisma-backed implementation of the
|
|
3
|
+
* SDK KillSwitchEvaluator contract introduced by QAP-085 / QAP-086 / QAP-087.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 Phase 4 (Policy Governance). Three independent triggers that
|
|
6
|
+
* halt auto-fix activity when recent telemetry indicates the platform is
|
|
7
|
+
* misbehaving:
|
|
8
|
+
*
|
|
9
|
+
* 1. SUCCESS_RATE_DROP (QAP-085) — per-(classification, surface):
|
|
10
|
+
* For each (classification, surface) seen in recent MERGED attempts
|
|
11
|
+
* (last 30 days), compute success rate as
|
|
12
|
+
* `(mergedClean + mergedWithEdits) / total`. If `total >= minSample`
|
|
13
|
+
* AND `successRate < threshold`, trip a CLASSIFICATION-scope switch.
|
|
14
|
+
*
|
|
15
|
+
* 2. CONSECUTIVE_FAILURE (QAP-086) — per-project: count attempts in
|
|
16
|
+
* the last `consecutiveFailureWindowHours` for the project ordered
|
|
17
|
+
* by attemptedAt desc. If the FIRST N are all PRE_VERIFY_FAILED,
|
|
18
|
+
* trip a PROJECT-scope switch.
|
|
19
|
+
*
|
|
20
|
+
* 3. REGRESSION_CASCADE (QAP-087) — platform-wide: count attempts
|
|
21
|
+
* across ALL projects where `autoRevertedAt` is within the last
|
|
22
|
+
* `regressionCascadeWindowHours`. If `>=N`, trip a PLATFORM-scope
|
|
23
|
+
* switch (projectId IS NULL).
|
|
24
|
+
*
|
|
25
|
+
* # History semantics
|
|
26
|
+
*
|
|
27
|
+
* Trips are history-preserving — when a trigger evaluates positive on a
|
|
28
|
+
* tuple that already has an engaged row, the existing row is preserved.
|
|
29
|
+
* When it evaluates positive on a tuple with only unblocked rows (or
|
|
30
|
+
* none), a new row is inserted. This means the audit trail shows every
|
|
31
|
+
* trip across time without losing prior unblock provenance.
|
|
32
|
+
*
|
|
33
|
+
* # Tenant isolation
|
|
34
|
+
*
|
|
35
|
+
* Every method scopes by projectId. Pattern D — wrong-project / unknown-
|
|
36
|
+
* project lookups for unblock return `not_found` (defense-in-depth
|
|
37
|
+
* against tenant enumeration). PLATFORM-scope switches have
|
|
38
|
+
* `projectId IS NULL` in storage; getEngaged unions per-project + platform
|
|
39
|
+
* rows so the dispatcher gate sees both.
|
|
40
|
+
*/
|
|
41
|
+
import { KillSwitchEvaluatorError, } from '@derwinjs/sdk';
|
|
42
|
+
// ─── Defaults ────────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Default thresholds applied when a project's `killSwitchConfig` JSON
|
|
45
|
+
* column is null. Operators tighten / loosen these per-project once the
|
|
46
|
+
* project has trust history. Picked conservatively so the platform errs
|
|
47
|
+
* on the side of halting too eagerly rather than missing a real problem.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_KILL_SWITCH_THRESHOLDS = {
|
|
50
|
+
// QAP-085 — SUCCESS_RATE_DROP
|
|
51
|
+
successRateDropPct: 0.4, // < 40% success in last N attempts → trip
|
|
52
|
+
successRateMinSample: 10, // need at least 10 attempts before evaluating
|
|
53
|
+
// QAP-086 — CONSECUTIVE_FAILURE
|
|
54
|
+
consecutiveFailureN: 5, // 5 PRE_VERIFY_FAILED in a row → trip
|
|
55
|
+
consecutiveFailureWindowHours: 24,
|
|
56
|
+
// QAP-087 — REGRESSION_CASCADE
|
|
57
|
+
regressionCascadeN: 3, // 3 new regressions across platform → trip
|
|
58
|
+
regressionCascadeWindowHours: 6,
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Window over which SUCCESS_RATE_DROP looks for merged attempts. The
|
|
62
|
+
* trust telemetry roll-up uses a 30-day window everywhere else (see
|
|
63
|
+
* ClassificationTrust.attemptsLast30d), so we mirror it here.
|
|
64
|
+
*/
|
|
65
|
+
const SUCCESS_RATE_LOOKBACK_DAYS = 30;
|
|
66
|
+
const MS_PER_HOUR = 60 * 60 * 1000;
|
|
67
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
68
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* Merge a project's killSwitchConfig JSON over the defaults. Only known
|
|
71
|
+
* keys are picked up; unknown keys are ignored. Non-finite / wrong-type
|
|
72
|
+
* values fall back to the default, so a malformed JSON column can't break
|
|
73
|
+
* the evaluator.
|
|
74
|
+
*/
|
|
75
|
+
function resolveThresholds(raw) {
|
|
76
|
+
const cfg = raw !== null && typeof raw === 'object' && !Array.isArray(raw)
|
|
77
|
+
? raw
|
|
78
|
+
: {};
|
|
79
|
+
function num(key) {
|
|
80
|
+
const v = cfg[key];
|
|
81
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
82
|
+
return v;
|
|
83
|
+
return DEFAULT_KILL_SWITCH_THRESHOLDS[key];
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
successRateDropPct: num('successRateDropPct'),
|
|
87
|
+
successRateMinSample: num('successRateMinSample'),
|
|
88
|
+
consecutiveFailureN: num('consecutiveFailureN'),
|
|
89
|
+
consecutiveFailureWindowHours: num('consecutiveFailureWindowHours'),
|
|
90
|
+
regressionCascadeN: num('regressionCascadeN'),
|
|
91
|
+
regressionCascadeWindowHours: num('regressionCascadeWindowHours'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// AttemptStatus values that count as "merged" for success-rate purposes.
|
|
95
|
+
const MERGED_STATUSES = ['AUTO_MERGED', 'HUMAN_MERGED'];
|
|
96
|
+
// AttemptStatus values that count as "failed-and-completed" for
|
|
97
|
+
// success-rate purposes (post-merge regressions).
|
|
98
|
+
const REGRESSED_STATUSES = ['REGRESSED_REVERTED'];
|
|
99
|
+
function mapRow(row) {
|
|
100
|
+
// triggerMetrics is stored as JSONB; coerce the Prisma JsonValue back
|
|
101
|
+
// into a plain object for the contract type. Defense-in-depth: if a
|
|
102
|
+
// legacy / direct-DB row has a non-object scalar, surface an empty
|
|
103
|
+
// object rather than letting downstream code crash on `.foo`.
|
|
104
|
+
const metrics = row.triggerMetrics !== null &&
|
|
105
|
+
typeof row.triggerMetrics === 'object' &&
|
|
106
|
+
!Array.isArray(row.triggerMetrics)
|
|
107
|
+
? row.triggerMetrics
|
|
108
|
+
: {};
|
|
109
|
+
return {
|
|
110
|
+
id: row.id,
|
|
111
|
+
projectId: row.projectId,
|
|
112
|
+
classification: row.classification,
|
|
113
|
+
surface: row.surface,
|
|
114
|
+
type: row.type,
|
|
115
|
+
scope: row.scope,
|
|
116
|
+
engaged: row.engaged,
|
|
117
|
+
engagedAt: row.engagedAt,
|
|
118
|
+
unblockedAt: row.unblockedAt,
|
|
119
|
+
unblockedBy: row.unblockedBy,
|
|
120
|
+
reason: row.reason,
|
|
121
|
+
triggerMetrics: metrics,
|
|
122
|
+
createdAt: row.createdAt,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
126
|
+
export function createPrismaKillSwitchEvaluator(config) {
|
|
127
|
+
const { prisma } = config;
|
|
128
|
+
const now = config.now ?? (() => new Date());
|
|
129
|
+
/**
|
|
130
|
+
* Insert a new engaged switch row IFF no engaged row already exists for
|
|
131
|
+
* the (projectId, classification, surface, type, scope) tuple. Returns
|
|
132
|
+
* the row regardless (the existing or the newly inserted one) so the
|
|
133
|
+
* caller can include it in the evaluate() return list.
|
|
134
|
+
*/
|
|
135
|
+
async function tripIfNotEngaged(args) {
|
|
136
|
+
// Find an existing engaged row for this tuple. We do NOT use
|
|
137
|
+
// findUnique because (type, scope, classification, surface, projectId)
|
|
138
|
+
// is not a unique constraint by design — we want the history-preserving
|
|
139
|
+
// append-on-retrip semantics.
|
|
140
|
+
const existing = await prisma.killSwitchState.findFirst({
|
|
141
|
+
where: {
|
|
142
|
+
projectId: args.projectId,
|
|
143
|
+
classification: args.classification,
|
|
144
|
+
surface: args.surface,
|
|
145
|
+
type: args.type,
|
|
146
|
+
scope: args.scope,
|
|
147
|
+
engaged: true,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
if (existing !== null)
|
|
151
|
+
return existing;
|
|
152
|
+
const created = await prisma.killSwitchState.create({
|
|
153
|
+
data: {
|
|
154
|
+
projectId: args.projectId,
|
|
155
|
+
classification: args.classification,
|
|
156
|
+
surface: args.surface,
|
|
157
|
+
type: args.type,
|
|
158
|
+
scope: args.scope,
|
|
159
|
+
engaged: true,
|
|
160
|
+
reason: args.reason,
|
|
161
|
+
triggerMetrics: args.triggerMetrics,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
return created;
|
|
165
|
+
}
|
|
166
|
+
// ── Trigger 1 — QAP-085 SUCCESS_RATE_DROP ──────────────────────────────
|
|
167
|
+
async function evaluateSuccessRateDrop(projectId, thresholds, at) {
|
|
168
|
+
const since = new Date(at.getTime() - SUCCESS_RATE_LOOKBACK_DAYS * MS_PER_DAY);
|
|
169
|
+
// Pull recent terminal-state attempts (merged or regressed-reverted).
|
|
170
|
+
// For each (classification, surface) tuple we compute success rate
|
|
171
|
+
// = merged / (merged + regressed). PRE_VERIFY_FAILED attempts never
|
|
172
|
+
// dispatched, so they are NOT in the denominator — they belong to
|
|
173
|
+
// the CONSECUTIVE_FAILURE trigger instead.
|
|
174
|
+
const attempts = await prisma.qAFixAttempt.findMany({
|
|
175
|
+
where: {
|
|
176
|
+
projectId,
|
|
177
|
+
attemptedAt: { gte: since },
|
|
178
|
+
dispatchStatus: {
|
|
179
|
+
in: [...MERGED_STATUSES, ...REGRESSED_STATUSES],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
select: {
|
|
183
|
+
dispatchStatus: true,
|
|
184
|
+
ticket: { select: { classification: true, surface: true } },
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const tallies = new Map();
|
|
188
|
+
for (const a of attempts) {
|
|
189
|
+
const classification = a.ticket.classification;
|
|
190
|
+
const surface = a.ticket.surface;
|
|
191
|
+
if (classification.length === 0)
|
|
192
|
+
continue;
|
|
193
|
+
const key = `${classification}::${surface}`;
|
|
194
|
+
let t = tallies.get(key);
|
|
195
|
+
if (t === undefined) {
|
|
196
|
+
t = { classification, surface, total: 0, merged: 0 };
|
|
197
|
+
tallies.set(key, t);
|
|
198
|
+
}
|
|
199
|
+
t.total += 1;
|
|
200
|
+
if (MERGED_STATUSES.includes(a.dispatchStatus)) {
|
|
201
|
+
t.merged += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const tripped = [];
|
|
205
|
+
for (const t of tallies.values()) {
|
|
206
|
+
if (t.total < thresholds.successRateMinSample)
|
|
207
|
+
continue;
|
|
208
|
+
const rate = t.merged / t.total;
|
|
209
|
+
if (rate >= thresholds.successRateDropPct)
|
|
210
|
+
continue;
|
|
211
|
+
const row = await tripIfNotEngaged({
|
|
212
|
+
projectId,
|
|
213
|
+
classification: t.classification,
|
|
214
|
+
surface: t.surface,
|
|
215
|
+
type: 'SUCCESS_RATE_DROP',
|
|
216
|
+
scope: 'CLASSIFICATION',
|
|
217
|
+
reason: `QAP-085: success rate ${(rate * 100).toFixed(1)}% over ${t.total.toString()} attempts ` +
|
|
218
|
+
`is below threshold ${(thresholds.successRateDropPct * 100).toFixed(0)}% ` +
|
|
219
|
+
`for ${t.classification} on ${t.surface}.`,
|
|
220
|
+
triggerMetrics: {
|
|
221
|
+
classification: t.classification,
|
|
222
|
+
surface: t.surface,
|
|
223
|
+
total: t.total,
|
|
224
|
+
merged: t.merged,
|
|
225
|
+
successRate: rate,
|
|
226
|
+
threshold: thresholds.successRateDropPct,
|
|
227
|
+
windowDays: SUCCESS_RATE_LOOKBACK_DAYS,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
tripped.push(row);
|
|
231
|
+
}
|
|
232
|
+
return tripped;
|
|
233
|
+
}
|
|
234
|
+
// ── Trigger 2 — QAP-086 CONSECUTIVE_FAILURE ─────────────────────────────
|
|
235
|
+
async function evaluateConsecutiveFailure(projectId, thresholds, at) {
|
|
236
|
+
const since = new Date(at.getTime() - thresholds.consecutiveFailureWindowHours * MS_PER_HOUR);
|
|
237
|
+
// Order by attemptedAt desc so we examine the most recent attempts
|
|
238
|
+
// first. We need at least N attempts in the window; if the first N
|
|
239
|
+
// (ordered by recency desc) are ALL PRE_VERIFY_FAILED, trip.
|
|
240
|
+
const recent = await prisma.qAFixAttempt.findMany({
|
|
241
|
+
where: {
|
|
242
|
+
projectId,
|
|
243
|
+
attemptedAt: { gte: since },
|
|
244
|
+
},
|
|
245
|
+
orderBy: { attemptedAt: 'desc' },
|
|
246
|
+
take: thresholds.consecutiveFailureN,
|
|
247
|
+
select: { id: true, dispatchStatus: true, attemptedAt: true },
|
|
248
|
+
});
|
|
249
|
+
if (recent.length < thresholds.consecutiveFailureN)
|
|
250
|
+
return [];
|
|
251
|
+
const allFailed = recent.every((a) => a.dispatchStatus === 'PRE_VERIFY_FAILED');
|
|
252
|
+
if (!allFailed)
|
|
253
|
+
return [];
|
|
254
|
+
const row = await tripIfNotEngaged({
|
|
255
|
+
projectId,
|
|
256
|
+
classification: null,
|
|
257
|
+
surface: null,
|
|
258
|
+
type: 'CONSECUTIVE_FAILURE',
|
|
259
|
+
scope: 'PROJECT',
|
|
260
|
+
reason: `QAP-086: ${thresholds.consecutiveFailureN.toString()} consecutive PRE_VERIFY_FAILED attempts ` +
|
|
261
|
+
`within ${thresholds.consecutiveFailureWindowHours.toString()}h.`,
|
|
262
|
+
triggerMetrics: {
|
|
263
|
+
consecutiveFailureN: thresholds.consecutiveFailureN,
|
|
264
|
+
windowHours: thresholds.consecutiveFailureWindowHours,
|
|
265
|
+
attemptIds: recent.map((a) => a.id),
|
|
266
|
+
oldestAttemptAt: recent[recent.length - 1]?.attemptedAt ?? null,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return [row];
|
|
270
|
+
}
|
|
271
|
+
// ── Trigger 3 — QAP-087 REGRESSION_CASCADE ──────────────────────────────
|
|
272
|
+
async function evaluateRegressionCascade(thresholds, at) {
|
|
273
|
+
const since = new Date(at.getTime() - thresholds.regressionCascadeWindowHours * MS_PER_HOUR);
|
|
274
|
+
// Platform-wide — count attempts across ALL projects that were
|
|
275
|
+
// auto-reverted within the window. autoRevertedAt is the field the
|
|
276
|
+
// Sprint 7 RegressionDetector flips when post-verify telemetry shows
|
|
277
|
+
// a regression.
|
|
278
|
+
const reverts = await prisma.qAFixAttempt.findMany({
|
|
279
|
+
where: {
|
|
280
|
+
autoRevertedAt: { gte: since, not: null },
|
|
281
|
+
},
|
|
282
|
+
select: { id: true, projectId: true, autoRevertedAt: true },
|
|
283
|
+
});
|
|
284
|
+
if (reverts.length < thresholds.regressionCascadeN)
|
|
285
|
+
return [];
|
|
286
|
+
const row = await tripIfNotEngaged({
|
|
287
|
+
projectId: null,
|
|
288
|
+
classification: null,
|
|
289
|
+
surface: null,
|
|
290
|
+
type: 'REGRESSION_CASCADE',
|
|
291
|
+
scope: 'PLATFORM',
|
|
292
|
+
reason: `QAP-087: ${reverts.length.toString()} regressions across the platform within ` +
|
|
293
|
+
`${thresholds.regressionCascadeWindowHours.toString()}h ` +
|
|
294
|
+
`(threshold ${thresholds.regressionCascadeN.toString()}).`,
|
|
295
|
+
triggerMetrics: {
|
|
296
|
+
regressionCount: reverts.length,
|
|
297
|
+
threshold: thresholds.regressionCascadeN,
|
|
298
|
+
windowHours: thresholds.regressionCascadeWindowHours,
|
|
299
|
+
attemptIds: reverts.map((r) => r.id),
|
|
300
|
+
affectedProjectIds: Array.from(new Set(reverts.map((r) => r.projectId))),
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
return [row];
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
async evaluate(input) {
|
|
307
|
+
if (typeof input.projectId !== 'string' || input.projectId.length === 0) {
|
|
308
|
+
throw new KillSwitchEvaluatorError('invalid_input', 'KillSwitchEvaluator: projectId is required');
|
|
309
|
+
}
|
|
310
|
+
const at = now();
|
|
311
|
+
// Resolve thresholds: project override → defaults. Pattern D —
|
|
312
|
+
// unknown project resolves to defaults; the trigger queries simply
|
|
313
|
+
// return zero rows for an unknown projectId, so no trip.
|
|
314
|
+
const project = await prisma.project.findUnique({
|
|
315
|
+
where: { id: input.projectId },
|
|
316
|
+
select: { killSwitchConfig: true },
|
|
317
|
+
});
|
|
318
|
+
const thresholds = resolveThresholds(project?.killSwitchConfig ?? null);
|
|
319
|
+
// Run all three triggers. They are independent — one tripping
|
|
320
|
+
// doesn't short-circuit the others.
|
|
321
|
+
const tripped = [];
|
|
322
|
+
tripped.push(...(await evaluateSuccessRateDrop(input.projectId, thresholds, at)));
|
|
323
|
+
tripped.push(...(await evaluateConsecutiveFailure(input.projectId, thresholds, at)));
|
|
324
|
+
tripped.push(...(await evaluateRegressionCascade(thresholds, at)));
|
|
325
|
+
// Return the union of (newly-tripped switches) ∪ (already-engaged
|
|
326
|
+
// switches that match this project). The caller (dispatcher) cares
|
|
327
|
+
// about what's CURRENTLY engaged, not strictly about the set we
|
|
328
|
+
// tripped on this evaluate() call.
|
|
329
|
+
const engaged = await this.getEngaged({ projectId: input.projectId });
|
|
330
|
+
// De-duplicate by id — tripped rows for THIS evaluate call may
|
|
331
|
+
// already be in the engaged list from getEngaged.
|
|
332
|
+
const byId = new Map();
|
|
333
|
+
for (const r of tripped)
|
|
334
|
+
byId.set(r.id, mapRow(r));
|
|
335
|
+
for (const e of engaged)
|
|
336
|
+
byId.set(e.id, e);
|
|
337
|
+
return Array.from(byId.values());
|
|
338
|
+
},
|
|
339
|
+
async getEngaged(input) {
|
|
340
|
+
if (typeof input.projectId !== 'string' || input.projectId.length === 0) {
|
|
341
|
+
throw new KillSwitchEvaluatorError('invalid_input', 'KillSwitchEvaluator: projectId is required');
|
|
342
|
+
}
|
|
343
|
+
// Union: (engaged switches scoped to this project) OR
|
|
344
|
+
// (engaged platform-wide switches, projectId IS NULL).
|
|
345
|
+
const rows = await prisma.killSwitchState.findMany({
|
|
346
|
+
where: {
|
|
347
|
+
engaged: true,
|
|
348
|
+
OR: [{ projectId: input.projectId }, { scope: 'PLATFORM' }],
|
|
349
|
+
},
|
|
350
|
+
orderBy: { engagedAt: 'desc' },
|
|
351
|
+
});
|
|
352
|
+
return rows.map(mapRow);
|
|
353
|
+
},
|
|
354
|
+
async unblock(input) {
|
|
355
|
+
if (typeof input.projectId !== 'string' || input.projectId.length === 0) {
|
|
356
|
+
throw new KillSwitchEvaluatorError('invalid_input', 'KillSwitchEvaluator: projectId is required');
|
|
357
|
+
}
|
|
358
|
+
if (typeof input.switchId !== 'string' || input.switchId.length === 0) {
|
|
359
|
+
throw new KillSwitchEvaluatorError('invalid_input', 'KillSwitchEvaluator: switchId is required');
|
|
360
|
+
}
|
|
361
|
+
if (typeof input.unblockedBy !== 'string' || input.unblockedBy.trim().length === 0) {
|
|
362
|
+
throw new KillSwitchEvaluatorError('invalid_input', 'KillSwitchEvaluator: unblockedBy is required');
|
|
363
|
+
}
|
|
364
|
+
// Pattern D — operator can only unblock a switch they have access to:
|
|
365
|
+
// 1. CLASSIFICATION / PROJECT switches must match projectId.
|
|
366
|
+
// 2. PLATFORM switches are unblockable from any project (any
|
|
367
|
+
// operator with platform-admin rights — middleware enforces).
|
|
368
|
+
const existing = await prisma.killSwitchState.findFirst({
|
|
369
|
+
where: {
|
|
370
|
+
id: input.switchId,
|
|
371
|
+
OR: [{ projectId: input.projectId }, { scope: 'PLATFORM' }],
|
|
372
|
+
},
|
|
373
|
+
select: { id: true },
|
|
374
|
+
});
|
|
375
|
+
if (existing === null) {
|
|
376
|
+
throw new KillSwitchEvaluatorError('not_found', `KillSwitch ${input.switchId} not found in project ${input.projectId}`);
|
|
377
|
+
}
|
|
378
|
+
await prisma.killSwitchState.update({
|
|
379
|
+
where: { id: input.switchId },
|
|
380
|
+
data: {
|
|
381
|
+
engaged: false,
|
|
382
|
+
unblockedAt: now(),
|
|
383
|
+
unblockedBy: input.unblockedBy,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=kill-switch-evaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kill-switch-evaluator.js","sourceRoot":"","sources":["../src/kill-switch-evaluator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaPathTierResolver — Prisma-backed implementation of the SDK
|
|
3
|
+
* PathTierResolver contract introduced by QAP-080.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 (Policy Governance, Phase 1). Resolves a list of paths to the
|
|
6
|
+
* **strictest** RiskTier among all rules that match any of the supplied
|
|
7
|
+
* paths. "Strictest" follows the RiskTier ladder: NEVER > HIGH > MEDIUM >
|
|
8
|
+
* LOW. Falls back to the project's `defaultTier` when no rule matches; if
|
|
9
|
+
* the project itself doesn't exist, falls back to LOW (defense-in-depth
|
|
10
|
+
* against tenant enumeration — wrong-project lookups must not throw or
|
|
11
|
+
* leak whether a Project row exists).
|
|
12
|
+
*
|
|
13
|
+
* No external glob library — `matchGlob` below is a small in-house helper
|
|
14
|
+
* that handles the three glob features we need (`**`, `*`, literals). We
|
|
15
|
+
* deliberately avoid pulling `minimatch` into @derwinjs/db to keep the
|
|
16
|
+
* dependency surface small for Sprint 8; if the Sprint 9 policy editor
|
|
17
|
+
* needs feature parity (negation, brace expansion, `?`), revisit then.
|
|
18
|
+
*
|
|
19
|
+
* Tenant isolation: every read scopes by projectId. setRules wraps DELETE
|
|
20
|
+
* + bulk INSERT in an interactive transaction so partial failures don't
|
|
21
|
+
* leave a half-applied ruleset.
|
|
22
|
+
*/
|
|
23
|
+
import type { PrismaClient } from './prisma.js';
|
|
24
|
+
import { type PathTierResolver } from '@derwinjs/sdk';
|
|
25
|
+
export interface PrismaPathTierResolverConfig {
|
|
26
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
27
|
+
prisma: PrismaClient;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Minimal glob → path matcher.
|
|
31
|
+
*
|
|
32
|
+
* `**` — matches zero or more path segments (including `/`)
|
|
33
|
+
* `*` — matches zero or more chars within a single segment (no `/`)
|
|
34
|
+
* anything else — literal
|
|
35
|
+
*
|
|
36
|
+
* Implemented by translating the glob to a RegExp. Anchored at both ends.
|
|
37
|
+
* This is intentionally a small subset — we don't support brace expansion,
|
|
38
|
+
* negation, character classes, or `?`. Add them when a real consumer
|
|
39
|
+
* needs them; YAGNI applies in Sprint 8.
|
|
40
|
+
*
|
|
41
|
+
* Throws PathTierResolverError(`invalid_pattern`) on a glob that produces
|
|
42
|
+
* an unparseable RegExp (extremely rare with the supported subset, but
|
|
43
|
+
* the guard exists so the contract's error-code surface is honored).
|
|
44
|
+
*/
|
|
45
|
+
export declare function matchGlob(pattern: string, path: string): boolean;
|
|
46
|
+
export declare function createPrismaPathTierResolver(config: PrismaPathTierResolverConfig): PathTierResolver;
|
|
47
|
+
//# sourceMappingURL=path-tier-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-tier-resolver.d.ts","sourceRoot":"","sources":["../src/path-tier-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAGL,KAAK,gBAAgB,EAGtB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,4BAA4B;IAC3C,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AA2BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CA0ChE;AAqBD,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,4BAA4B,GACnC,gBAAgB,CAuElB"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaPathTierResolver — Prisma-backed implementation of the SDK
|
|
3
|
+
* PathTierResolver contract introduced by QAP-080.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 (Policy Governance, Phase 1). Resolves a list of paths to the
|
|
6
|
+
* **strictest** RiskTier among all rules that match any of the supplied
|
|
7
|
+
* paths. "Strictest" follows the RiskTier ladder: NEVER > HIGH > MEDIUM >
|
|
8
|
+
* LOW. Falls back to the project's `defaultTier` when no rule matches; if
|
|
9
|
+
* the project itself doesn't exist, falls back to LOW (defense-in-depth
|
|
10
|
+
* against tenant enumeration — wrong-project lookups must not throw or
|
|
11
|
+
* leak whether a Project row exists).
|
|
12
|
+
*
|
|
13
|
+
* No external glob library — `matchGlob` below is a small in-house helper
|
|
14
|
+
* that handles the three glob features we need (`**`, `*`, literals). We
|
|
15
|
+
* deliberately avoid pulling `minimatch` into @derwinjs/db to keep the
|
|
16
|
+
* dependency surface small for Sprint 8; if the Sprint 9 policy editor
|
|
17
|
+
* needs feature parity (negation, brace expansion, `?`), revisit then.
|
|
18
|
+
*
|
|
19
|
+
* Tenant isolation: every read scopes by projectId. setRules wraps DELETE
|
|
20
|
+
* + bulk INSERT in an interactive transaction so partial failures don't
|
|
21
|
+
* leave a half-applied ruleset.
|
|
22
|
+
*/
|
|
23
|
+
import { PathTierResolverError, RiskTierValues, } from '@derwinjs/sdk';
|
|
24
|
+
// ─── Tier ordering ────────────────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Strictness ladder. The resolver picks the maximum-strictness tier among
|
|
27
|
+
* all matching rules. NEVER is strictest (forbidden), LOW is loosest.
|
|
28
|
+
*
|
|
29
|
+
* NOTE: the SDK's RiskTier enum currently is `LOW | MEDIUM | HIGH | NEVER`.
|
|
30
|
+
* The Sprint 8 plan referenced `CRITICAL` in early drafts; we honor the
|
|
31
|
+
* existing schema enum and treat NEVER as the strictest tier (it means
|
|
32
|
+
* "auto-fix forbidden — must be human-only"). If a future migration
|
|
33
|
+
* renames NEVER to CRITICAL, update this ladder accordingly.
|
|
34
|
+
*/
|
|
35
|
+
const TIER_STRICTNESS = {
|
|
36
|
+
LOW: 0,
|
|
37
|
+
MEDIUM: 1,
|
|
38
|
+
HIGH: 2,
|
|
39
|
+
NEVER: 3,
|
|
40
|
+
};
|
|
41
|
+
function strictest(a, b) {
|
|
42
|
+
return TIER_STRICTNESS[a] >= TIER_STRICTNESS[b] ? a : b;
|
|
43
|
+
}
|
|
44
|
+
// ─── Glob matcher ─────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Minimal glob → path matcher.
|
|
47
|
+
*
|
|
48
|
+
* `**` — matches zero or more path segments (including `/`)
|
|
49
|
+
* `*` — matches zero or more chars within a single segment (no `/`)
|
|
50
|
+
* anything else — literal
|
|
51
|
+
*
|
|
52
|
+
* Implemented by translating the glob to a RegExp. Anchored at both ends.
|
|
53
|
+
* This is intentionally a small subset — we don't support brace expansion,
|
|
54
|
+
* negation, character classes, or `?`. Add them when a real consumer
|
|
55
|
+
* needs them; YAGNI applies in Sprint 8.
|
|
56
|
+
*
|
|
57
|
+
* Throws PathTierResolverError(`invalid_pattern`) on a glob that produces
|
|
58
|
+
* an unparseable RegExp (extremely rare with the supported subset, but
|
|
59
|
+
* the guard exists so the contract's error-code surface is honored).
|
|
60
|
+
*/
|
|
61
|
+
export function matchGlob(pattern, path) {
|
|
62
|
+
// Tokenize so we can distinguish `**` from two consecutive `*`.
|
|
63
|
+
let regexSrc = '';
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < pattern.length) {
|
|
66
|
+
const ch = pattern.charAt(i);
|
|
67
|
+
if (ch === '*') {
|
|
68
|
+
if (pattern[i + 1] === '*') {
|
|
69
|
+
// `**` — match anything including `/`
|
|
70
|
+
regexSrc += '.*';
|
|
71
|
+
i += 2;
|
|
72
|
+
// Skip a trailing `/` in `**/` so the rule matches the empty
|
|
73
|
+
// prefix case (e.g., `**/foo.ts` should match `foo.ts`).
|
|
74
|
+
if (pattern[i] === '/') {
|
|
75
|
+
regexSrc += '/?';
|
|
76
|
+
i += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// single `*` — match within a segment
|
|
81
|
+
regexSrc += '[^/]*';
|
|
82
|
+
i += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (/[.+?^${}()|[\]\\]/.test(ch)) {
|
|
86
|
+
regexSrc += '\\' + ch;
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
regexSrc += ch;
|
|
91
|
+
i += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let re;
|
|
95
|
+
try {
|
|
96
|
+
re = new RegExp('^' + regexSrc + '$');
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
throw new PathTierResolverError('invalid_pattern', `PathTierResolver: invalid glob pattern ${JSON.stringify(pattern)}`, err);
|
|
100
|
+
}
|
|
101
|
+
return re.test(path);
|
|
102
|
+
}
|
|
103
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
104
|
+
function validateRule(rule) {
|
|
105
|
+
if (typeof rule.pattern !== 'string' || rule.pattern.length === 0) {
|
|
106
|
+
throw new PathTierResolverError('invalid_pattern', 'PathTierResolver: rule.pattern must be a non-empty string');
|
|
107
|
+
}
|
|
108
|
+
if (!RiskTierValues.includes(rule.tier)) {
|
|
109
|
+
throw new PathTierResolverError('invalid_tier', `PathTierResolver: rule.tier must be one of ${RiskTierValues.join(', ')} (got ${rule.tier})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
113
|
+
export function createPrismaPathTierResolver(config) {
|
|
114
|
+
const { prisma } = config;
|
|
115
|
+
return {
|
|
116
|
+
async resolveTier(input) {
|
|
117
|
+
const { projectId, paths } = input;
|
|
118
|
+
// Read project row for the fallback tier. findUnique (not findFirst)
|
|
119
|
+
// because Project.id is the unique key. Wrong-project / unknown-
|
|
120
|
+
// project returns null → fall back to LOW (no leak that the project
|
|
121
|
+
// doesn't exist).
|
|
122
|
+
const project = await prisma.project.findUnique({
|
|
123
|
+
where: { id: projectId },
|
|
124
|
+
select: { defaultTier: true },
|
|
125
|
+
});
|
|
126
|
+
const fallback = project?.defaultTier ?? 'LOW';
|
|
127
|
+
if (paths.length === 0) {
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
const rules = await prisma.pathTierRule.findMany({
|
|
131
|
+
where: { projectId },
|
|
132
|
+
orderBy: { priority: 'asc' },
|
|
133
|
+
});
|
|
134
|
+
let strictestSeen = null;
|
|
135
|
+
for (const rule of rules) {
|
|
136
|
+
const matched = paths.some((p) => matchGlob(rule.pattern, p));
|
|
137
|
+
if (matched) {
|
|
138
|
+
strictestSeen = strictestSeen === null ? rule.tier : strictest(strictestSeen, rule.tier);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return strictestSeen ?? fallback;
|
|
142
|
+
},
|
|
143
|
+
async listRules(input) {
|
|
144
|
+
const rows = await prisma.pathTierRule.findMany({
|
|
145
|
+
where: { projectId: input.projectId },
|
|
146
|
+
orderBy: { priority: 'asc' },
|
|
147
|
+
});
|
|
148
|
+
return rows.map((row) => ({
|
|
149
|
+
pattern: row.pattern,
|
|
150
|
+
tier: row.tier,
|
|
151
|
+
...(row.reason !== null ? { reason: row.reason } : {}),
|
|
152
|
+
}));
|
|
153
|
+
},
|
|
154
|
+
async setRules(input) {
|
|
155
|
+
const { projectId, rules } = input;
|
|
156
|
+
// Validate up-front so a bad rule fails fast before we touch the DB.
|
|
157
|
+
for (const rule of rules)
|
|
158
|
+
validateRule(rule);
|
|
159
|
+
// Atomic replace: DELETE + bulk INSERT inside one transaction.
|
|
160
|
+
await prisma.$transaction(async (tx) => {
|
|
161
|
+
await tx.pathTierRule.deleteMany({ where: { projectId } });
|
|
162
|
+
if (rules.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
await tx.pathTierRule.createMany({
|
|
165
|
+
data: rules.map((rule, idx) => ({
|
|
166
|
+
projectId,
|
|
167
|
+
pattern: rule.pattern,
|
|
168
|
+
tier: rule.tier,
|
|
169
|
+
priority: idx,
|
|
170
|
+
reason: rule.reason ?? null,
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=path-tier-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-tier-resolver.js","sourceRoot":"","sources":["../src/path-tier-resolver.ts"],"names":[],"mappings":"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"}
|