@derwinjs/db 0.6.0 → 0.8.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/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/drift-detector.d.ts +63 -0
- package/dist/drift-detector.d.ts.map +1 -0
- package/dist/drift-detector.js +0 -0
- package/dist/drift-detector.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/rag-retriever.d.ts.map +1 -1
- package/dist/rag-retriever.js +38 -0
- package/dist/rag-retriever.js.map +1 -1
- package/dist/regression-detector.d.ts +45 -0
- package/dist/regression-detector.d.ts.map +1 -0
- package/dist/regression-detector.js +78 -0
- package/dist/regression-detector.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,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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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default DriftDetector — QAP-Sprint7 Phase 6 / QAP-077.
|
|
3
|
+
*
|
|
4
|
+
* Stock implementation of the SDK DriftDetector contract
|
|
5
|
+
* (packages/sdk/src/types/drift-detector.ts). Compares per-(classification,
|
|
6
|
+
* surface) success rates over a recent CURRENT window against a longer
|
|
7
|
+
* BASELINE window (the period immediately preceding the current window) and
|
|
8
|
+
* emits a DriftAlarm[] for tuples whose current rate is materially worse
|
|
9
|
+
* than baseline.
|
|
10
|
+
*
|
|
11
|
+
* Window semantics:
|
|
12
|
+
* t0 = now
|
|
13
|
+
* currentStart = t0 - currentWindowDays
|
|
14
|
+
* baselineStart = currentStart - baselineWindowDays
|
|
15
|
+
*
|
|
16
|
+
* currentWindow = [currentStart, t0)
|
|
17
|
+
* baselineWindow = [baselineStart, currentStart)
|
|
18
|
+
*
|
|
19
|
+
* The two windows are NON-overlapping by construction so an attempt counts in
|
|
20
|
+
* exactly one window; the comparison is genuinely "recent vs prior" rather
|
|
21
|
+
* than "recent vs trailing-30-day-including-this-week".
|
|
22
|
+
*
|
|
23
|
+
* Sample selection mirrors LearningHealthReporter (Sprint 2 Group D-3):
|
|
24
|
+
* - dispatchStatus IN ('AUTO_MERGED', 'HUMAN_MERGED') — Lifeline's MERGED
|
|
25
|
+
* under Option β
|
|
26
|
+
* - mergedAt within the window
|
|
27
|
+
* - fixSuccessScore not null — only attempts that have been scored count
|
|
28
|
+
* toward the success rate; pending-outcome attempts are excluded from
|
|
29
|
+
* both numerator and denominator (otherwise a brand-new merge with no
|
|
30
|
+
* score yet would be miscounted as a failure).
|
|
31
|
+
*
|
|
32
|
+
* Success classification:
|
|
33
|
+
* fixSuccessScore > 0 → success (Layer F scoring tiers: +0.2 / +0.5 / +1.0)
|
|
34
|
+
* fixSuccessScore <= 0 → failure (-1.0 / -0.5 / 0.0)
|
|
35
|
+
*
|
|
36
|
+
* classification + surface live on QATicket, not QAFixAttempt, so the query
|
|
37
|
+
* pulls them via the `ticket` relation in a single findMany then aggregates
|
|
38
|
+
* in-memory. For Sprint 7 cron usage this is fine (tens to low-hundreds of
|
|
39
|
+
* MERGED attempts per project per 37-day span); a future scale-out could
|
|
40
|
+
* push the grouping into a raw SQL aggregate over the join.
|
|
41
|
+
*
|
|
42
|
+
* Failure semantics: throws DriftDetectorError('invalid_input') on malformed
|
|
43
|
+
* input (negative window, empty projectId). Empty projects return [].
|
|
44
|
+
*
|
|
45
|
+
* Tenant isolation: projectId flows directly into the Prisma `where` clause,
|
|
46
|
+
* matching the rest of Layer F.
|
|
47
|
+
*/
|
|
48
|
+
import type { PrismaClient } from './prisma.js';
|
|
49
|
+
import { type DriftDetector } from '@derwinjs/sdk';
|
|
50
|
+
export interface PrismaDriftDetectorConfig {
|
|
51
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
52
|
+
prisma: PrismaClient;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build a DriftDetector backed by Prisma over QAFixAttempt + QATicket.
|
|
56
|
+
*
|
|
57
|
+
* The returned `scan` is intended to be invoked from a periodic job
|
|
58
|
+
* (mirroring OutcomeRecorder + TrustScoreUpdater from QAP-075). Sprint 8
|
|
59
|
+
* may expose `GET /qa/drift-alarms` if operator-facing UI needs it; the
|
|
60
|
+
* scan itself stays cron-callable independent of an HTTP route.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createPrismaDriftDetector(config: PrismaDriftDetectorConfig): DriftDetector;
|
|
63
|
+
//# sourceMappingURL=drift-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drift-detector.d.ts","sourceRoot":"","sources":["../src/drift-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,yBAAyB;IACxC,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAYD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,yBAAyB,GAAG,aAAa,CA6G1F"}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drift-detector.js","sourceRoot":"","sources":["../src/drift-detector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaFreezeWindowEvaluator — Prisma-backed implementation of the
|
|
3
|
+
* SDK FreezeWindowEvaluator contract introduced by QAP-083.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 Phase 2 (Policy Governance). Operator-defined recurring time
|
|
6
|
+
* windows during which auto-fix dispatch is partially or fully blocked.
|
|
7
|
+
*
|
|
8
|
+
* # Cron evaluator
|
|
9
|
+
*
|
|
10
|
+
* Embedded, dependency-free. Supports a SUBSET of standard 5-field cron:
|
|
11
|
+
* `minute hour dayOfMonth month dayOfWeek`. Each field accepts:
|
|
12
|
+
* - `*` — wildcard (any value in the field's allowed range)
|
|
13
|
+
* - exact non-negative integer in the field's allowed range
|
|
14
|
+
*
|
|
15
|
+
* NOT supported (intentional MVP scope): `*\/N` step values, `M-N` ranges,
|
|
16
|
+
* comma-separated lists, named months/days, `?`, `L`, `W`, `#`. Sprint 9's
|
|
17
|
+
* policy editor may extend this when richer scheduling is needed; until
|
|
18
|
+
* then, operators compose multiple FreezeWindow rows for OR semantics
|
|
19
|
+
* (e.g., separate rows for "every Friday 5pm" and "every Saturday 9am").
|
|
20
|
+
*
|
|
21
|
+
* Algorithm: `mostRecentFire(cron, at)` walks BACKWARDS minute-by-minute
|
|
22
|
+
* from `at` for at most `MAX_LOOKBACK_MINUTES` and returns the first time
|
|
23
|
+
* the cron matches. The dispatcher only ever asks "did this fire within
|
|
24
|
+
* the past durationMinutes?"; we cap the search at 7 days of minutes
|
|
25
|
+
* (10080) which comfortably exceeds any practical durationMinutes.
|
|
26
|
+
*
|
|
27
|
+
* # Tenant isolation
|
|
28
|
+
*
|
|
29
|
+
* Every method scopes by projectId. Pattern D — wrong-project /
|
|
30
|
+
* unknown-project queries return empty arrays / not_found errors.
|
|
31
|
+
*/
|
|
32
|
+
import type { PrismaClient } from './prisma.js';
|
|
33
|
+
import { type FreezeWindowEvaluator } from '@derwinjs/sdk';
|
|
34
|
+
export interface PrismaFreezeWindowEvaluatorConfig {
|
|
35
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
36
|
+
prisma: PrismaClient;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parsed cron — each field is either null (wildcard `*`) or a literal int.
|
|
40
|
+
*/
|
|
41
|
+
interface ParsedCron {
|
|
42
|
+
minute: number | null;
|
|
43
|
+
hour: number | null;
|
|
44
|
+
dayOfMonth: number | null;
|
|
45
|
+
month: number | null;
|
|
46
|
+
dayOfWeek: number | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse a 5-field cron expression. Throws FreezeWindowEvaluatorError
|
|
50
|
+
* (`invalid_cron`) on bad input. Whitespace-tolerant — splits on /\s+/.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseCron(expr: string): ParsedCron;
|
|
53
|
+
/**
|
|
54
|
+
* Find the most recent time at or before `at` when the cron fires.
|
|
55
|
+
* Returns null if no fire within the past MAX_LOOKBACK_MINUTES (which
|
|
56
|
+
* exceeds any practical durationMinutes — windows beyond a week are out
|
|
57
|
+
* of scope for the MVP).
|
|
58
|
+
*/
|
|
59
|
+
export declare function mostRecentFire(cron: ParsedCron, at: Date): Date | null;
|
|
60
|
+
export declare function createPrismaFreezeWindowEvaluator(config: PrismaFreezeWindowEvaluatorConfig): FreezeWindowEvaluator;
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=freeze-window-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freeze-window-evaluator.d.ts","sourceRoot":"","sources":["../src/freeze-window-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,iCAAiC;IAChD,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAmBD;;GAEG;AACH,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAyClD;AAgCD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAoBtE;AA+ED,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,iCAAiC,GACxC,qBAAqB,CA4DvB"}
|