@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,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaProjectModeStore — Prisma-backed implementation of the SDK
|
|
3
|
+
* ProjectModeStore contract introduced by QAP-090.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 9 (Policy + UI, Phase 1). Single choke point for reading and
|
|
6
|
+
* writing `Project.mode` (OBSERVE / TICKET_ONLY / AUTHOR / AUTO — see
|
|
7
|
+
* product brief §11.2). Every mode change goes through `setMode` so the
|
|
8
|
+
* audit log (ProjectModeLog) stays in lockstep with the Project row.
|
|
9
|
+
*
|
|
10
|
+
* Tenant isolation: every read scopes by projectId. `getMode` returns
|
|
11
|
+
* null for unknown projectId; `listLog` returns an empty array. `setMode`
|
|
12
|
+
* throws `project_not_found` (rather than returning null) because a
|
|
13
|
+
* mode-change call against a missing project is a caller bug, not a
|
|
14
|
+
* cross-tenant probe — this matches the operator UX of the policy editor.
|
|
15
|
+
*
|
|
16
|
+
* Atomicity: `setMode` wraps (a) read fromMode → (b) update Project.mode +
|
|
17
|
+
* modeChangedAt + modeChangedBy → (c) insert ProjectModeLog row in a
|
|
18
|
+
* single Prisma interactive transaction so a partial write never leaves
|
|
19
|
+
* the audit log out of sync with the Project row.
|
|
20
|
+
*/
|
|
21
|
+
import type { PrismaClient } from './prisma.js';
|
|
22
|
+
import { type ProjectModeStore } from '@derwinjs/sdk';
|
|
23
|
+
export interface PrismaProjectModeStoreConfig {
|
|
24
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
25
|
+
prisma: PrismaClient;
|
|
26
|
+
}
|
|
27
|
+
export declare function createPrismaProjectModeStore(config: PrismaProjectModeStoreConfig): ProjectModeStore;
|
|
28
|
+
//# sourceMappingURL=project-mode-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-mode-store.d.ts","sourceRoot":"","sources":["../src/project-mode-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAML,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,4BAA4B;IAC3C,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AAgED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,4BAA4B,GACnC,gBAAgB,CA+ElB"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaProjectModeStore — Prisma-backed implementation of the SDK
|
|
3
|
+
* ProjectModeStore contract introduced by QAP-090.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 9 (Policy + UI, Phase 1). Single choke point for reading and
|
|
6
|
+
* writing `Project.mode` (OBSERVE / TICKET_ONLY / AUTHOR / AUTO — see
|
|
7
|
+
* product brief §11.2). Every mode change goes through `setMode` so the
|
|
8
|
+
* audit log (ProjectModeLog) stays in lockstep with the Project row.
|
|
9
|
+
*
|
|
10
|
+
* Tenant isolation: every read scopes by projectId. `getMode` returns
|
|
11
|
+
* null for unknown projectId; `listLog` returns an empty array. `setMode`
|
|
12
|
+
* throws `project_not_found` (rather than returning null) because a
|
|
13
|
+
* mode-change call against a missing project is a caller bug, not a
|
|
14
|
+
* cross-tenant probe — this matches the operator UX of the policy editor.
|
|
15
|
+
*
|
|
16
|
+
* Atomicity: `setMode` wraps (a) read fromMode → (b) update Project.mode +
|
|
17
|
+
* modeChangedAt + modeChangedBy → (c) insert ProjectModeLog row in a
|
|
18
|
+
* single Prisma interactive transaction so a partial write never leaves
|
|
19
|
+
* the audit log out of sync with the Project row.
|
|
20
|
+
*/
|
|
21
|
+
import { ProjectModeStoreError, ProjectModeValues, } from '@derwinjs/sdk';
|
|
22
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
23
|
+
function validateSetInput(input) {
|
|
24
|
+
if (typeof input.projectId !== 'string' || input.projectId.length === 0) {
|
|
25
|
+
throw new ProjectModeStoreError('invalid_input', 'ProjectModeStore: projectId is required');
|
|
26
|
+
}
|
|
27
|
+
if (!ProjectModeValues.includes(input.toMode)) {
|
|
28
|
+
throw new ProjectModeStoreError('invalid_mode', `ProjectModeStore: toMode must be one of ${ProjectModeValues.join(', ')} (got ${input.toMode})`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof input.reason !== 'string' || input.reason.trim().length === 0) {
|
|
31
|
+
throw new ProjectModeStoreError('invalid_input', 'ProjectModeStore: reason is required');
|
|
32
|
+
}
|
|
33
|
+
if (typeof input.changedBy !== 'string' || input.changedBy.trim().length === 0) {
|
|
34
|
+
throw new ProjectModeStoreError('invalid_input', 'ProjectModeStore: changedBy is required');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function mapProjectRow(row) {
|
|
38
|
+
return {
|
|
39
|
+
projectId: row.id,
|
|
40
|
+
mode: row.mode,
|
|
41
|
+
modeChangedAt: row.modeChangedAt,
|
|
42
|
+
modeChangedBy: row.modeChangedBy,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function mapLogRow(row) {
|
|
46
|
+
return {
|
|
47
|
+
id: row.id,
|
|
48
|
+
projectId: row.projectId,
|
|
49
|
+
fromMode: row.fromMode,
|
|
50
|
+
toMode: row.toMode,
|
|
51
|
+
reason: row.reason,
|
|
52
|
+
changedBy: row.changedBy,
|
|
53
|
+
changedAt: row.changedAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
57
|
+
export function createPrismaProjectModeStore(config) {
|
|
58
|
+
const { prisma } = config;
|
|
59
|
+
return {
|
|
60
|
+
async getMode(input) {
|
|
61
|
+
const row = await prisma.project.findUnique({
|
|
62
|
+
where: { id: input.projectId },
|
|
63
|
+
select: {
|
|
64
|
+
id: true,
|
|
65
|
+
mode: true,
|
|
66
|
+
modeChangedAt: true,
|
|
67
|
+
modeChangedBy: true,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (row === null)
|
|
71
|
+
return null;
|
|
72
|
+
return mapProjectRow(row);
|
|
73
|
+
},
|
|
74
|
+
async setMode(input) {
|
|
75
|
+
validateSetInput(input);
|
|
76
|
+
// Atomic: read fromMode → update Project → insert log row.
|
|
77
|
+
// Interactive transaction so all three steps share a snapshot and
|
|
78
|
+
// commit together.
|
|
79
|
+
const updated = await prisma.$transaction(async (tx) => {
|
|
80
|
+
const existing = await tx.project.findUnique({
|
|
81
|
+
where: { id: input.projectId },
|
|
82
|
+
select: { id: true, mode: true },
|
|
83
|
+
});
|
|
84
|
+
if (existing === null) {
|
|
85
|
+
throw new ProjectModeStoreError('project_not_found', `ProjectModeStore: project ${input.projectId} not found`);
|
|
86
|
+
}
|
|
87
|
+
const fromMode = existing.mode;
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const next = await tx.project.update({
|
|
90
|
+
where: { id: input.projectId },
|
|
91
|
+
data: {
|
|
92
|
+
mode: input.toMode,
|
|
93
|
+
modeChangedAt: now,
|
|
94
|
+
modeChangedBy: input.changedBy,
|
|
95
|
+
},
|
|
96
|
+
select: {
|
|
97
|
+
id: true,
|
|
98
|
+
mode: true,
|
|
99
|
+
modeChangedAt: true,
|
|
100
|
+
modeChangedBy: true,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
await tx.projectModeLog.create({
|
|
104
|
+
data: {
|
|
105
|
+
projectId: input.projectId,
|
|
106
|
+
fromMode,
|
|
107
|
+
toMode: input.toMode,
|
|
108
|
+
reason: input.reason,
|
|
109
|
+
changedBy: input.changedBy,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
return next;
|
|
113
|
+
});
|
|
114
|
+
return mapProjectRow(updated);
|
|
115
|
+
},
|
|
116
|
+
async listLog(input) {
|
|
117
|
+
const rows = await prisma.projectModeLog.findMany({
|
|
118
|
+
where: { projectId: input.projectId },
|
|
119
|
+
orderBy: { changedAt: 'desc' },
|
|
120
|
+
take: input.limit ?? 50,
|
|
121
|
+
});
|
|
122
|
+
return rows.map(mapLogRow);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=project-mode-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-mode-store.js","sourceRoot":"","sources":["../src/project-mode-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaTrustThresholdConfigStore — Prisma-backed implementation of
|
|
3
|
+
* the SDK TrustThresholdConfigStore contract introduced by QAP-082.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 Phase 2 (Policy Governance). The config is stored as a JSON
|
|
6
|
+
* column on Project.trustThresholds. Null in storage means "use defaults".
|
|
7
|
+
*
|
|
8
|
+
* Tenant isolation: every method scopes by projectId. Pattern D applies —
|
|
9
|
+
* unknown / wrong-project lookups return DEFAULT_TRUST_THRESHOLDS rather
|
|
10
|
+
* than throwing or leaking existence (defense-in-depth against tenant
|
|
11
|
+
* enumeration).
|
|
12
|
+
*/
|
|
13
|
+
import type { PrismaClient } from './prisma.js';
|
|
14
|
+
import { type TrustThresholdConfigStore } from '@derwinjs/sdk';
|
|
15
|
+
export interface PrismaTrustThresholdConfigStoreConfig {
|
|
16
|
+
/** Generated Prisma client. Pass an instance per process. */
|
|
17
|
+
prisma: PrismaClient;
|
|
18
|
+
}
|
|
19
|
+
export declare function createPrismaTrustThresholdConfigStore(config: PrismaTrustThresholdConfigStoreConfig): TrustThresholdConfigStore;
|
|
20
|
+
//# sourceMappingURL=trust-threshold-config-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trust-threshold-config-store.d.ts","sourceRoot":"","sources":["../src/trust-threshold-config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAIL,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,qCAAqC;IACpD,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAC;CACtB;AA8DD,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,qCAAqC,GAC5C,yBAAyB,CA4B3B"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPrismaTrustThresholdConfigStore — Prisma-backed implementation of
|
|
3
|
+
* the SDK TrustThresholdConfigStore contract introduced by QAP-082.
|
|
4
|
+
*
|
|
5
|
+
* Sprint 8 Phase 2 (Policy Governance). The config is stored as a JSON
|
|
6
|
+
* column on Project.trustThresholds. Null in storage means "use defaults".
|
|
7
|
+
*
|
|
8
|
+
* Tenant isolation: every method scopes by projectId. Pattern D applies —
|
|
9
|
+
* unknown / wrong-project lookups return DEFAULT_TRUST_THRESHOLDS rather
|
|
10
|
+
* than throwing or leaking existence (defense-in-depth against tenant
|
|
11
|
+
* enumeration).
|
|
12
|
+
*/
|
|
13
|
+
import { DEFAULT_TRUST_THRESHOLDS, TrustThresholdConfigError, } from '@derwinjs/sdk';
|
|
14
|
+
// ─── Validation ──────────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Each threshold must be a finite integer in [0, 100]. We accept floats
|
|
17
|
+
* because the JSON column can store them, but the dispatcher's comparison
|
|
18
|
+
* is integer-percent semantically; reject NaN / Infinity / out-of-range.
|
|
19
|
+
*/
|
|
20
|
+
function validateConfig(config) {
|
|
21
|
+
for (const [key, value] of [
|
|
22
|
+
['low', config.low],
|
|
23
|
+
['medium', config.medium],
|
|
24
|
+
['high', config.high],
|
|
25
|
+
]) {
|
|
26
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
27
|
+
throw new TrustThresholdConfigError('invalid_input', `TrustThresholdConfig: ${key} must be a finite number (got ${String(value)})`);
|
|
28
|
+
}
|
|
29
|
+
if (value < 0 || value > 100) {
|
|
30
|
+
throw new TrustThresholdConfigError('invalid_input', `TrustThresholdConfig: ${key} must be in [0, 100] (got ${String(value)})`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort coercion of an unknown JSON value into a TrustThresholdConfig.
|
|
36
|
+
* Returns null if the shape is wrong — caller falls back to defaults. We
|
|
37
|
+
* intentionally do not throw here: defense-in-depth so a corrupted row
|
|
38
|
+
* doesn't cascade into a runtime error.
|
|
39
|
+
*/
|
|
40
|
+
function tryParseConfig(value) {
|
|
41
|
+
if (value !== null &&
|
|
42
|
+
typeof value === 'object' &&
|
|
43
|
+
!Array.isArray(value) &&
|
|
44
|
+
'low' in value &&
|
|
45
|
+
'medium' in value &&
|
|
46
|
+
'high' in value) {
|
|
47
|
+
const v = value;
|
|
48
|
+
if (typeof v.low === 'number' &&
|
|
49
|
+
typeof v.medium === 'number' &&
|
|
50
|
+
typeof v.high === 'number' &&
|
|
51
|
+
Number.isFinite(v.low) &&
|
|
52
|
+
Number.isFinite(v.medium) &&
|
|
53
|
+
Number.isFinite(v.high)) {
|
|
54
|
+
return { low: v.low, medium: v.medium, high: v.high };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// ─── Factory ─────────────────────────────────────────────────────────────
|
|
60
|
+
export function createPrismaTrustThresholdConfigStore(config) {
|
|
61
|
+
const { prisma } = config;
|
|
62
|
+
return {
|
|
63
|
+
async getConfig(input) {
|
|
64
|
+
const project = await prisma.project.findUnique({
|
|
65
|
+
where: { id: input.projectId },
|
|
66
|
+
select: { trustThresholds: true },
|
|
67
|
+
});
|
|
68
|
+
if (project === null)
|
|
69
|
+
return { ...DEFAULT_TRUST_THRESHOLDS };
|
|
70
|
+
const parsed = tryParseConfig(project.trustThresholds);
|
|
71
|
+
return parsed ?? { ...DEFAULT_TRUST_THRESHOLDS };
|
|
72
|
+
},
|
|
73
|
+
async setConfig(input) {
|
|
74
|
+
validateConfig(input.config);
|
|
75
|
+
await prisma.project.update({
|
|
76
|
+
where: { id: input.projectId },
|
|
77
|
+
data: {
|
|
78
|
+
trustThresholds: {
|
|
79
|
+
low: input.config.low,
|
|
80
|
+
medium: input.config.medium,
|
|
81
|
+
high: input.config.high,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=trust-threshold-config-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trust-threshold-config-store.js","sourceRoot":"","sources":["../src/trust-threshold-config-store.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@derwinjs/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Prisma schema + migrations for Derwin's own Postgres. 14 models, project-namespaced. Per ADR-0005. Ships its own generated Prisma client (multi-platform binaries) for cross-consumer compatibility.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@prisma/client": "^5.22.0",
|
|
36
|
-
"@derwinjs/core": "0.
|
|
37
|
-
"@derwinjs/sdk": "0.
|
|
36
|
+
"@derwinjs/core": "0.9.0",
|
|
37
|
+
"@derwinjs/sdk": "0.9.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@vitest/coverage-v8": "^2.1.9",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- Sprint 8 (QAP-080..081) — Policy governance: path-tier rules + classification override table.
|
|
2
|
+
--
|
|
3
|
+
-- Promotes Policy.pathRules / Policy.classificationOverrides JSON columns to
|
|
4
|
+
-- first-class tables so the policy-editor UI (Sprint 9 / QAP-088) can render,
|
|
5
|
+
-- reorder, and audit them as discrete rows. The legacy JSON fields on Policy
|
|
6
|
+
-- stay for back-compat with QAP-013's createPrismaFixPolicy until Sprint 9
|
|
7
|
+
-- migrates the read path.
|
|
8
|
+
--
|
|
9
|
+
-- Idempotent (IF NOT EXISTS) — safe to re-run on environments where the
|
|
10
|
+
-- enum or tables already exist.
|
|
11
|
+
|
|
12
|
+
-- 1. Project.defaultTier — fallback tier when no PathTierRule matches.
|
|
13
|
+
ALTER TABLE "derwin"."projects"
|
|
14
|
+
ADD COLUMN IF NOT EXISTS "defaultTier" "derwin"."RiskTier" NOT NULL DEFAULT 'LOW';
|
|
15
|
+
|
|
16
|
+
-- 2. OverrideMode enum.
|
|
17
|
+
DO $$ BEGIN
|
|
18
|
+
CREATE TYPE "derwin"."OverrideMode" AS ENUM ('BLOCK_AUTO_FIX', 'FORCE_ESCALATION', 'DOWNGRADE_TIER');
|
|
19
|
+
EXCEPTION
|
|
20
|
+
WHEN duplicate_object THEN NULL;
|
|
21
|
+
END $$;
|
|
22
|
+
|
|
23
|
+
-- 3. PathTierRule.
|
|
24
|
+
CREATE TABLE IF NOT EXISTS "derwin"."path_tier_rules" (
|
|
25
|
+
"id" TEXT PRIMARY KEY,
|
|
26
|
+
"projectId" TEXT NOT NULL,
|
|
27
|
+
"pattern" TEXT NOT NULL,
|
|
28
|
+
"tier" "derwin"."RiskTier" NOT NULL,
|
|
29
|
+
"priority" INTEGER NOT NULL,
|
|
30
|
+
"reason" TEXT,
|
|
31
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
32
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
33
|
+
CONSTRAINT "path_tier_rules_projectId_fkey"
|
|
34
|
+
FOREIGN KEY ("projectId") REFERENCES "derwin"."projects"("id")
|
|
35
|
+
ON DELETE CASCADE ON UPDATE CASCADE
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS "path_tier_rules_projectId_priority_idx"
|
|
39
|
+
ON "derwin"."path_tier_rules" ("projectId", "priority");
|
|
40
|
+
|
|
41
|
+
-- 4. ClassificationOverride.
|
|
42
|
+
CREATE TABLE IF NOT EXISTS "derwin"."classification_overrides" (
|
|
43
|
+
"id" TEXT PRIMARY KEY,
|
|
44
|
+
"projectId" TEXT NOT NULL,
|
|
45
|
+
"classification" TEXT NOT NULL,
|
|
46
|
+
"surface" TEXT,
|
|
47
|
+
"overrideMode" "derwin"."OverrideMode" NOT NULL,
|
|
48
|
+
"downgradeTier" "derwin"."RiskTier",
|
|
49
|
+
"reason" TEXT NOT NULL,
|
|
50
|
+
"createdBy" TEXT NOT NULL,
|
|
51
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
52
|
+
CONSTRAINT "classification_overrides_projectId_fkey"
|
|
53
|
+
FOREIGN KEY ("projectId") REFERENCES "derwin"."projects"("id")
|
|
54
|
+
ON DELETE CASCADE ON UPDATE CASCADE
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS "classification_overrides_projectId_classification_surface_idx"
|
|
58
|
+
ON "derwin"."classification_overrides" ("projectId", "classification", "surface");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- Sprint 8 Phase 2 (QAP-082..083) — Trust threshold config + freeze windows.
|
|
2
|
+
--
|
|
3
|
+
-- Phase 1 (20260507120000_sprint8_policy_governance) added PathTierRule,
|
|
4
|
+
-- ClassificationOverride, and Project.defaultTier. Phase 2 layers on:
|
|
5
|
+
-- 1. Project.trustThresholds JSONB — per-tier minimum trustPercent for
|
|
6
|
+
-- auto-fix dispatch. Null → use SDK defaults ({low:60, medium:80, high:95}).
|
|
7
|
+
-- 2. freeze_windows table — operator-defined recurring dispatch freeze
|
|
8
|
+
-- windows. Cron-driven, 5-field, no seconds.
|
|
9
|
+
--
|
|
10
|
+
-- Idempotent (IF NOT EXISTS). Safe to re-run on environments where Phase 1
|
|
11
|
+
-- already landed but Phase 2 didn't.
|
|
12
|
+
|
|
13
|
+
-- 1. Project.trustThresholds — JSONB, nullable.
|
|
14
|
+
ALTER TABLE "derwin"."projects"
|
|
15
|
+
ADD COLUMN IF NOT EXISTS "trustThresholds" JSONB;
|
|
16
|
+
|
|
17
|
+
-- 2. FreezeWindow.
|
|
18
|
+
CREATE TABLE IF NOT EXISTS "derwin"."freeze_windows" (
|
|
19
|
+
"id" TEXT PRIMARY KEY,
|
|
20
|
+
"projectId" TEXT NOT NULL,
|
|
21
|
+
"cronExpression" TEXT NOT NULL,
|
|
22
|
+
"durationMinutes" INTEGER NOT NULL,
|
|
23
|
+
"label" TEXT NOT NULL,
|
|
24
|
+
"blocksDispatch" BOOLEAN NOT NULL DEFAULT TRUE,
|
|
25
|
+
"enabled" BOOLEAN NOT NULL DEFAULT TRUE,
|
|
26
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
27
|
+
CONSTRAINT "freeze_windows_projectId_fkey"
|
|
28
|
+
FOREIGN KEY ("projectId") REFERENCES "derwin"."projects"("id")
|
|
29
|
+
ON DELETE CASCADE ON UPDATE CASCADE
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS "freeze_windows_projectId_enabled_idx"
|
|
33
|
+
ON "derwin"."freeze_windows" ("projectId", "enabled");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Sprint 8 Phase 3 (QAP-084) — Per-project budget cap + soft-warn pct.
|
|
2
|
+
--
|
|
3
|
+
-- Adds the budget-gate columns the BudgetGate contract reads:
|
|
4
|
+
-- 1. Project.monthlyBudgetUsd — DOUBLE PRECISION, nullable. Null = NO_CAP.
|
|
5
|
+
-- 2. Project.budgetSoftWarnPct — DOUBLE PRECISION, default 0.8 (80%).
|
|
6
|
+
--
|
|
7
|
+
-- The dispatcher consults a BudgetGate before runFixCycle and refuses new
|
|
8
|
+
-- auto-fix dispatches when month-to-date spend / monthlyBudgetUsd >= 1.0.
|
|
9
|
+
-- Soft-warn (>= budgetSoftWarnPct, < 1.0) is observable but non-blocking;
|
|
10
|
+
-- hard-stop (>= 1.0) blocks. NO_CAP (monthlyBudgetUsd IS NULL) is the
|
|
11
|
+
-- pre-Phase-3 default and preserves legacy behavior for projects that have
|
|
12
|
+
-- not opted into explicit USD caps.
|
|
13
|
+
--
|
|
14
|
+
-- Idempotent (IF NOT EXISTS). Safe to re-run on environments where Phase 1
|
|
15
|
+
-- and Phase 2 have already landed but Phase 3 has not.
|
|
16
|
+
|
|
17
|
+
ALTER TABLE "derwin"."projects"
|
|
18
|
+
ADD COLUMN IF NOT EXISTS "monthlyBudgetUsd" DOUBLE PRECISION;
|
|
19
|
+
|
|
20
|
+
ALTER TABLE "derwin"."projects"
|
|
21
|
+
ADD COLUMN IF NOT EXISTS "budgetSoftWarnPct" DOUBLE PRECISION NOT NULL DEFAULT 0.8;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- Sprint 8 Phase 4 (QAP-085 / QAP-086 / QAP-087) — Three kill switches.
|
|
2
|
+
--
|
|
3
|
+
-- Adds the schema the KillSwitchEvaluator contract reads/writes:
|
|
4
|
+
-- 1. Project.killSwitchConfig — JSONB, nullable. Per-project threshold
|
|
5
|
+
-- overrides; null → use SDK defaults.
|
|
6
|
+
-- 2. KillSwitchType enum — three trigger discriminators.
|
|
7
|
+
-- 3. KillSwitchScope enum — three granularity discriminators.
|
|
8
|
+
-- 4. kill_switch_states table — history-preserving log of every trip.
|
|
9
|
+
--
|
|
10
|
+
-- The dispatcher consults a KillSwitchEvaluator before runFixCycle and
|
|
11
|
+
-- refuses to dispatch when any matching engaged switch is present:
|
|
12
|
+
-- - PROJECT or PLATFORM-scope switches block before ticket fetch.
|
|
13
|
+
-- - CLASSIFICATION-scope switches block after ticket fetch when the
|
|
14
|
+
-- ticket's classification + surface match the engaged switch's tuple.
|
|
15
|
+
--
|
|
16
|
+
-- History semantics: a new trip inserts a new row; an unblock flips
|
|
17
|
+
-- engaged → false on the existing row; subsequent re-trips on the same
|
|
18
|
+
-- tuple insert another row. This preserves the audit trail.
|
|
19
|
+
--
|
|
20
|
+
-- Idempotent (IF NOT EXISTS / DO ... EXCEPTION). Safe to re-run on
|
|
21
|
+
-- environments where Phase 1 / Phase 2 / Phase 3 already landed but
|
|
22
|
+
-- Phase 4 has not.
|
|
23
|
+
|
|
24
|
+
-- 1. Project.killSwitchConfig — JSONB, nullable.
|
|
25
|
+
ALTER TABLE "derwin"."projects"
|
|
26
|
+
ADD COLUMN IF NOT EXISTS "killSwitchConfig" JSONB;
|
|
27
|
+
|
|
28
|
+
-- 2. KillSwitchType enum.
|
|
29
|
+
DO $$ BEGIN
|
|
30
|
+
CREATE TYPE "derwin"."KillSwitchType" AS ENUM (
|
|
31
|
+
'SUCCESS_RATE_DROP',
|
|
32
|
+
'CONSECUTIVE_FAILURE',
|
|
33
|
+
'REGRESSION_CASCADE'
|
|
34
|
+
);
|
|
35
|
+
EXCEPTION
|
|
36
|
+
WHEN duplicate_object THEN NULL;
|
|
37
|
+
END $$;
|
|
38
|
+
|
|
39
|
+
-- 3. KillSwitchScope enum.
|
|
40
|
+
DO $$ BEGIN
|
|
41
|
+
CREATE TYPE "derwin"."KillSwitchScope" AS ENUM (
|
|
42
|
+
'CLASSIFICATION',
|
|
43
|
+
'PROJECT',
|
|
44
|
+
'PLATFORM'
|
|
45
|
+
);
|
|
46
|
+
EXCEPTION
|
|
47
|
+
WHEN duplicate_object THEN NULL;
|
|
48
|
+
END $$;
|
|
49
|
+
|
|
50
|
+
-- 4. KillSwitchState table.
|
|
51
|
+
CREATE TABLE IF NOT EXISTS "derwin"."kill_switch_states" (
|
|
52
|
+
"id" TEXT PRIMARY KEY,
|
|
53
|
+
"projectId" TEXT,
|
|
54
|
+
"classification" TEXT,
|
|
55
|
+
"surface" TEXT,
|
|
56
|
+
"type" "derwin"."KillSwitchType" NOT NULL,
|
|
57
|
+
"scope" "derwin"."KillSwitchScope" NOT NULL,
|
|
58
|
+
"engaged" BOOLEAN NOT NULL DEFAULT TRUE,
|
|
59
|
+
"engagedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
60
|
+
"unblockedAt" TIMESTAMP(3),
|
|
61
|
+
"unblockedBy" TEXT,
|
|
62
|
+
"reason" TEXT NOT NULL,
|
|
63
|
+
"triggerMetrics" JSONB NOT NULL,
|
|
64
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
65
|
+
CONSTRAINT "kill_switch_states_projectId_fkey"
|
|
66
|
+
FOREIGN KEY ("projectId") REFERENCES "derwin"."projects"("id")
|
|
67
|
+
ON DELETE CASCADE ON UPDATE CASCADE
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS "kill_switch_states_projectId_engaged_idx"
|
|
71
|
+
ON "derwin"."kill_switch_states" ("projectId", "engaged");
|
|
72
|
+
|
|
73
|
+
CREATE INDEX IF NOT EXISTS "kill_switch_states_type_scope_engaged_idx"
|
|
74
|
+
ON "derwin"."kill_switch_states" ("type", "scope", "engaged");
|