forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { createHash } = require('node:crypto');
|
|
7
|
+
|
|
8
|
+
const { ISSUE_STATUSES } = require('./issue-command-contract');
|
|
9
|
+
const { CLAIM_STATES, isClaimableType, isTerminalStatus } = require('./taxonomy-validator');
|
|
10
|
+
const REQUIRED_CLAIM_COLUMNS = Object.freeze([
|
|
11
|
+
'id', 'issue_id', 'actor', 'state', 'session_id', 'worktree_id', 'claimed_at', 'expires_at',
|
|
12
|
+
]);
|
|
13
|
+
const REQUIRED_ISSUE_COLUMNS = Object.freeze(['id', 'status', 'type']);
|
|
14
|
+
const REQUIRED_INDEXES = Object.freeze({
|
|
15
|
+
idx_kernel_claims_active_lease: Object.freeze({ columns: ['issue_id'], unique: true, partial: true }),
|
|
16
|
+
idx_kernel_claims_actor_state: Object.freeze({ columns: ['actor', 'state'], unique: false, partial: false }),
|
|
17
|
+
idx_kernel_claims_issue_state: Object.freeze({ columns: ['issue_id', 'state'], unique: false, partial: false }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
class ClaimRepairError extends Error {
|
|
21
|
+
constructor(code, message, details = {}) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'ClaimRepairError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.details = details;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function canonicalIso(value) {
|
|
30
|
+
if (typeof value !== 'string') return false;
|
|
31
|
+
const millis = Date.parse(value);
|
|
32
|
+
return Number.isFinite(millis) && new Date(millis).toISOString() === value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compareCanonical(left, right) {
|
|
36
|
+
const leftValue = String(left);
|
|
37
|
+
const rightValue = String(right);
|
|
38
|
+
if (leftValue < rightValue) return -1;
|
|
39
|
+
if (leftValue > rightValue) return 1;
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stableValue(value) {
|
|
44
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
45
|
+
if (!value || typeof value !== 'object') return value;
|
|
46
|
+
const sorted = {};
|
|
47
|
+
for (const key of Object.keys(value).sort(compareCanonical)) {
|
|
48
|
+
sorted[key] = stableValue(value[key]);
|
|
49
|
+
}
|
|
50
|
+
return sorted;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stableStringify(value) {
|
|
54
|
+
return JSON.stringify(stableValue(value));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256(value) {
|
|
58
|
+
return createHash('sha256').update(value).digest('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeIndex(index = {}) {
|
|
62
|
+
return {
|
|
63
|
+
name: String(index.name || ''),
|
|
64
|
+
columns: Array.isArray(index.columns) ? index.columns.map(String) : [],
|
|
65
|
+
unique: Boolean(index.unique),
|
|
66
|
+
partial: Boolean(index.partial),
|
|
67
|
+
sql: String(index.sql || '').replace(/\s+/g, ' ').trim(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectSchemaErrors(snapshot) {
|
|
72
|
+
const errors = [];
|
|
73
|
+
if (snapshot.integrity !== 'ok') errors.push('integrity_check');
|
|
74
|
+
if (snapshot.foreign_keys_enabled !== true) errors.push('foreign_keys_disabled');
|
|
75
|
+
if (Number(snapshot.foreign_key_faults || 0) !== 0) errors.push('foreign_key_fault');
|
|
76
|
+
if (Array.isArray(snapshot.read_errors) && snapshot.read_errors.length > 0) errors.push('schema_read_failure');
|
|
77
|
+
|
|
78
|
+
const claimColumns = new Set(snapshot.claim_columns || []);
|
|
79
|
+
const issueColumns = new Set(snapshot.issue_columns || []);
|
|
80
|
+
if (REQUIRED_CLAIM_COLUMNS.some(column => !claimColumns.has(column))) errors.push('claim_schema_columns');
|
|
81
|
+
if (REQUIRED_ISSUE_COLUMNS.some(column => !issueColumns.has(column))) errors.push('issue_schema_columns');
|
|
82
|
+
|
|
83
|
+
const foreignKeys = snapshot.claim_foreign_keys || [];
|
|
84
|
+
if (!foreignKeys.some(row => row.table === 'kernel_issues' && row.from === 'issue_id' && row.to === 'id')) {
|
|
85
|
+
errors.push('claim_issue_foreign_key');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const indexes = new Map((snapshot.claim_indexes || []).map(index => [index.name, normalizeIndex(index)]));
|
|
89
|
+
for (const [name, expected] of Object.entries(REQUIRED_INDEXES)) {
|
|
90
|
+
const actual = indexes.get(name);
|
|
91
|
+
if (!actual
|
|
92
|
+
|| stableStringify(actual.columns) !== stableStringify(expected.columns)
|
|
93
|
+
|| actual.unique !== expected.unique
|
|
94
|
+
|| actual.partial !== expected.partial) {
|
|
95
|
+
errors.push(`index:${name}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const activeIndex = indexes.get('idx_kernel_claims_active_lease');
|
|
99
|
+
if (activeIndex && !/\bon\s+kernel_claims\s*\(\s*issue_id\s*\)\s+where\s+state\s*=\s*'active'\s*;?$/i.test(activeIndex.sql)) {
|
|
100
|
+
errors.push('index:idx_kernel_claims_active_lease_sql');
|
|
101
|
+
}
|
|
102
|
+
return errors;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function exactClaimRow(row) {
|
|
106
|
+
return {
|
|
107
|
+
id: row.id,
|
|
108
|
+
issue_id: row.issue_id,
|
|
109
|
+
actor: row.actor,
|
|
110
|
+
state: row.state,
|
|
111
|
+
session_id: row.session_id ?? null,
|
|
112
|
+
worktree_id: row.worktree_id ?? null,
|
|
113
|
+
claimed_at: row.claimed_at,
|
|
114
|
+
expires_at: row.expires_at ?? null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function digestSnapshot(snapshot, observedAt, claims) {
|
|
119
|
+
const indexes = (snapshot.claim_indexes || []).map(normalizeIndex)
|
|
120
|
+
.sort((left, right) => compareCanonical(left.name, right.name));
|
|
121
|
+
const referencedIssueIds = new Set(claims.map(claim => claim.issue_id));
|
|
122
|
+
const issues = (snapshot.issues || [])
|
|
123
|
+
.filter(issue => referencedIssueIds.has(issue.id))
|
|
124
|
+
.map(issue => ({ id: issue.id, status: issue.status, type: issue.type }))
|
|
125
|
+
.sort((left, right) => compareCanonical(left.id, right.id));
|
|
126
|
+
const canonicalClaims = claims.map(exactClaimRow)
|
|
127
|
+
.sort((left, right) => compareCanonical(left.id, right.id));
|
|
128
|
+
const claimsById = new Map(canonicalClaims.map(claim => [claim.id, claim]));
|
|
129
|
+
const authoritySchema = [...(snapshot.authority_schema || [])]
|
|
130
|
+
.map(row => ({ type: row.type, name: row.name, table_name: row.tbl_name, sql: row.sql ?? null }))
|
|
131
|
+
.sort((left, right) => compareCanonical(stableStringify(left), stableStringify(right)));
|
|
132
|
+
const authorityTables = [...(snapshot.authority_tables || [])]
|
|
133
|
+
.map(table => {
|
|
134
|
+
const rows = (table.rows || []).map(row => {
|
|
135
|
+
if (table.name !== 'kernel_claims') return stableValue(row);
|
|
136
|
+
const claim = claimsById.get(row.id);
|
|
137
|
+
return stableValue(claim ? { ...row, state: claim.state } : row);
|
|
138
|
+
}).sort((left, right) => compareCanonical(stableStringify(left), stableStringify(right)));
|
|
139
|
+
return { name: table.name, rows };
|
|
140
|
+
})
|
|
141
|
+
.sort((left, right) => compareCanonical(left.name, right.name));
|
|
142
|
+
return sha256(stableStringify({
|
|
143
|
+
schema_version: 'forge.claim-repair.snapshot.v2',
|
|
144
|
+
observed_at: observedAt,
|
|
145
|
+
authority: { schema: authoritySchema, tables: authorityTables },
|
|
146
|
+
schema: {
|
|
147
|
+
claim_columns: [...(snapshot.claim_columns || [])]
|
|
148
|
+
.sort(compareCanonical),
|
|
149
|
+
issue_columns: [...(snapshot.issue_columns || [])]
|
|
150
|
+
.sort(compareCanonical),
|
|
151
|
+
claim_foreign_keys: [...(snapshot.claim_foreign_keys || [])]
|
|
152
|
+
.map(row => ({ table: row.table, from: row.from, to: row.to }))
|
|
153
|
+
.sort((left, right) => compareCanonical(stableStringify(left), stableStringify(right))),
|
|
154
|
+
claim_indexes: indexes,
|
|
155
|
+
},
|
|
156
|
+
issues,
|
|
157
|
+
claims: canonicalClaims,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateIssueRows(issues, errors) {
|
|
162
|
+
const issuesById = new Map();
|
|
163
|
+
for (const issue of issues) {
|
|
164
|
+
if (typeof issue.id !== 'string' || issue.id.length === 0 || !ISSUE_STATUSES.includes(issue.status)) {
|
|
165
|
+
errors.push('invalid_issue_state');
|
|
166
|
+
}
|
|
167
|
+
if (issuesById.has(issue.id)) errors.push('duplicate_issue_id');
|
|
168
|
+
issuesById.set(issue.id, issue);
|
|
169
|
+
}
|
|
170
|
+
return issuesById;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function claimRowErrors(claim, claimIds, issuesById) {
|
|
174
|
+
const errors = [];
|
|
175
|
+
if (typeof claim.id !== 'string' || claim.id.length === 0 || claimIds.has(claim.id)) {
|
|
176
|
+
errors.push('duplicate_or_invalid_claim_id');
|
|
177
|
+
}
|
|
178
|
+
if (typeof claim.issue_id !== 'string' || !issuesById.has(claim.issue_id)) errors.push('orphan_claim');
|
|
179
|
+
if (typeof claim.actor !== 'string' || claim.actor.trim() === '') errors.push('invalid_claim_actor');
|
|
180
|
+
if (!CLAIM_STATES.includes(claim.state)) errors.push('invalid_claim_state');
|
|
181
|
+
if (!canonicalIso(claim.claimed_at)) errors.push('invalid_claimed_at');
|
|
182
|
+
if (claim.expires_at !== null && claim.expires_at !== undefined && !canonicalIso(claim.expires_at)) {
|
|
183
|
+
errors.push('invalid_expires_at');
|
|
184
|
+
}
|
|
185
|
+
return errors;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validateClaimRows(claims, issuesById, errors) {
|
|
189
|
+
const claimIds = new Set();
|
|
190
|
+
const activeByIssue = new Map();
|
|
191
|
+
for (const claim of claims) {
|
|
192
|
+
errors.push(...claimRowErrors(claim, claimIds, issuesById));
|
|
193
|
+
claimIds.add(claim.id);
|
|
194
|
+
if (claim.state === 'active') {
|
|
195
|
+
activeByIssue.set(claim.issue_id, Number(activeByIssue.get(claim.issue_id) || 0) + 1);
|
|
196
|
+
const issue = issuesById.get(claim.issue_id);
|
|
197
|
+
if (issue && !isClaimableType(issue.type)) errors.push('unclaimable_active_claim');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if ([...activeByIssue.values()].some(count => count > 1)) errors.push('duplicate_active_claim');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function throwClaimRepairErrors(errors) {
|
|
204
|
+
if (errors.length === 0) return;
|
|
205
|
+
const counts = Object.create(null);
|
|
206
|
+
for (const error of errors) counts[error] = Number(counts[error] || 0) + 1;
|
|
207
|
+
const labels = Object.keys(counts).sort(compareCanonical);
|
|
208
|
+
throw new ClaimRepairError(
|
|
209
|
+
'CLAIM_REPAIR_PREFLIGHT_FAILED',
|
|
210
|
+
`Claim repair preflight failed closed (${labels.join(', ')})`,
|
|
211
|
+
{ errors: counts },
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function classifyClaimActions(claims, issuesById, observedAt) {
|
|
216
|
+
const actions = [];
|
|
217
|
+
let preservedNullExpiry = 0;
|
|
218
|
+
let preservedUnexpired = 0;
|
|
219
|
+
for (const claim of claims) {
|
|
220
|
+
if (claim.state !== 'active') continue;
|
|
221
|
+
const issue = issuesById.get(claim.issue_id);
|
|
222
|
+
if (isTerminalStatus(issue.status)) {
|
|
223
|
+
actions.push({ claim: exactClaimRow(claim), issue_status: issue.status, to_state: 'released' });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (claim.expires_at !== null && claim.expires_at !== undefined
|
|
227
|
+
&& Date.parse(claim.expires_at) <= Date.parse(observedAt)) {
|
|
228
|
+
actions.push({ claim: exactClaimRow(claim), issue_status: issue.status, to_state: 'reclaimable' });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (claim.expires_at === null || claim.expires_at === undefined) preservedNullExpiry += 1;
|
|
232
|
+
else preservedUnexpired += 1;
|
|
233
|
+
}
|
|
234
|
+
return { actions, preservedNullExpiry, preservedUnexpired };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildClaimRepairCounts(issues, claims, classification) {
|
|
238
|
+
const { actions, preservedNullExpiry, preservedUnexpired } = classification;
|
|
239
|
+
const released = actions.filter(action => action.to_state === 'released').length;
|
|
240
|
+
const reclaimable = actions.filter(action => action.to_state === 'reclaimable').length;
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
total_issues: issues.length,
|
|
243
|
+
total_claims: claims.length,
|
|
244
|
+
active_claims: claims.filter(claim => claim.state === 'active').length,
|
|
245
|
+
non_active_claims: claims.filter(claim => claim.state !== 'active').length,
|
|
246
|
+
terminal_active_to_release: released,
|
|
247
|
+
expired_nonterminal_to_reclaimable: reclaimable,
|
|
248
|
+
preserved_null_expiry_active: preservedNullExpiry,
|
|
249
|
+
preserved_unexpired_active: preservedUnexpired,
|
|
250
|
+
planned_mutations: actions.length,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildClaimRepairPlan(snapshot, options = {}) {
|
|
255
|
+
const observedAt = options.observedAt;
|
|
256
|
+
if (!canonicalIso(observedAt)) {
|
|
257
|
+
throw new ClaimRepairError(
|
|
258
|
+
'CLAIM_REPAIR_INVALID_OBSERVED_AT',
|
|
259
|
+
'Claim repair requires an exact canonical UTC timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)',
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const errors = collectSchemaErrors(snapshot);
|
|
264
|
+
const issues = Array.isArray(snapshot.issues) ? snapshot.issues : [];
|
|
265
|
+
const claims = Array.isArray(snapshot.claims) ? snapshot.claims : [];
|
|
266
|
+
const issuesById = validateIssueRows(issues, errors);
|
|
267
|
+
validateClaimRows(claims, issuesById, errors);
|
|
268
|
+
throwClaimRepairErrors(errors);
|
|
269
|
+
const classification = classifyClaimActions(claims, issuesById, observedAt);
|
|
270
|
+
const { actions } = classification;
|
|
271
|
+
|
|
272
|
+
const digest = digestSnapshot(snapshot, observedAt, claims);
|
|
273
|
+
const actionById = new Map(actions.map(action => [action.claim.id, action]));
|
|
274
|
+
const afterClaims = claims.map(claim => {
|
|
275
|
+
const action = actionById.get(claim.id);
|
|
276
|
+
return action ? { ...claim, state: action.to_state } : claim;
|
|
277
|
+
});
|
|
278
|
+
const afterDigest = digestSnapshot(snapshot, observedAt, afterClaims);
|
|
279
|
+
return {
|
|
280
|
+
observedAt,
|
|
281
|
+
digest,
|
|
282
|
+
afterDigest,
|
|
283
|
+
counts: buildClaimRepairCounts(issues, claims, classification),
|
|
284
|
+
actions,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function publicClaimRepairPreflight(plan) {
|
|
289
|
+
return Object.freeze({
|
|
290
|
+
schema_version: 'forge.claim-repair.preflight.v1',
|
|
291
|
+
mode: 'dry-run',
|
|
292
|
+
observed_at: plan.observedAt,
|
|
293
|
+
digest: plan.digest,
|
|
294
|
+
after_digest: plan.afterDigest,
|
|
295
|
+
counts: plan.counts,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function hashFile(filePath) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const hash = createHash('sha256');
|
|
302
|
+
const stream = fs.createReadStream(filePath);
|
|
303
|
+
stream.on('error', reject);
|
|
304
|
+
stream.on('data', chunk => hash.update(chunk));
|
|
305
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function cleanupRestoreProofDirectory(directory, fsApi = fs) {
|
|
310
|
+
try {
|
|
311
|
+
fsApi.rmSync(directory, {
|
|
312
|
+
recursive: true,
|
|
313
|
+
force: true,
|
|
314
|
+
maxRetries: 5,
|
|
315
|
+
retryDelay: 100,
|
|
316
|
+
});
|
|
317
|
+
} catch {
|
|
318
|
+
// A closed SQLite handle can remain briefly locked on Windows. The proof is
|
|
319
|
+
// already valid and the private OS temp directory can be reaped later.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function prepareRestoreProofSnapshot(backupPath, restoreDir, restorePath, options = {}) {
|
|
324
|
+
const fsApi = options.fsApi || fs;
|
|
325
|
+
const hardenPath = options.hardenPath;
|
|
326
|
+
const hardenPaths = options.hardenPaths;
|
|
327
|
+
if (typeof hardenPath !== 'function') {
|
|
328
|
+
throw new ClaimRepairError(
|
|
329
|
+
'CLAIM_REPAIR_BACKUP_PERMISSIONS',
|
|
330
|
+
'Restore proof requires a platform permission hardener',
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (!options.restoreDirHardened) {
|
|
334
|
+
if (typeof hardenPaths === 'function') await hardenPaths([restoreDir]);
|
|
335
|
+
else await hardenPath(restoreDir);
|
|
336
|
+
}
|
|
337
|
+
fsApi.copyFileSync(backupPath, restorePath, fs.constants.COPYFILE_EXCL);
|
|
338
|
+
if (typeof hardenPaths === 'function') await hardenPaths([restorePath]);
|
|
339
|
+
else await hardenPath(restorePath);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function verifyClaimRepairBackup(options = {}) {
|
|
343
|
+
const { backupPath, observedAt, openDriver, hardenPath, hardenPaths } = options;
|
|
344
|
+
const batchHardenPath = hardenPaths || hardenPath?.batch;
|
|
345
|
+
if (!backupPath || typeof openDriver !== 'function' || !fs.existsSync(backupPath)) {
|
|
346
|
+
throw new ClaimRepairError('CLAIM_REPAIR_BACKUP_REQUIRED', 'A readable separate SQLite backup is required');
|
|
347
|
+
}
|
|
348
|
+
if (typeof hardenPath !== 'function') {
|
|
349
|
+
throw new ClaimRepairError(
|
|
350
|
+
'CLAIM_REPAIR_BACKUP_PERMISSIONS',
|
|
351
|
+
'Backup verification requires a platform permission hardener',
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const restoreDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-claim-restore-proof-'));
|
|
355
|
+
const restorePath = path.join(restoreDir, 'kernel.restored.sqlite');
|
|
356
|
+
let restoreDriver;
|
|
357
|
+
try {
|
|
358
|
+
if (typeof batchHardenPath === 'function') await batchHardenPath([backupPath, restoreDir]);
|
|
359
|
+
else {
|
|
360
|
+
await hardenPath(backupPath);
|
|
361
|
+
await hardenPath(restoreDir);
|
|
362
|
+
}
|
|
363
|
+
for (const suffix of ['-wal', '-shm', '-journal']) {
|
|
364
|
+
if (fs.existsSync(`${backupPath}${suffix}`)) {
|
|
365
|
+
throw new ClaimRepairError('CLAIM_REPAIR_BACKUP_DRIFT', 'Backup sidecars are not allowed during restore proof');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
await prepareRestoreProofSnapshot(backupPath, restoreDir, restorePath, {
|
|
369
|
+
hardenPath,
|
|
370
|
+
hardenPaths: batchHardenPath,
|
|
371
|
+
restoreDirHardened: typeof batchHardenPath === 'function',
|
|
372
|
+
});
|
|
373
|
+
restoreDriver = openDriver(restorePath);
|
|
374
|
+
const restored = await restoreDriver.preflightLegacyClaimRepair({ observedAt });
|
|
375
|
+
const restoredHash = await hashFile(restorePath);
|
|
376
|
+
const currentBackupHash = await hashFile(backupPath);
|
|
377
|
+
if (currentBackupHash !== restoredHash) {
|
|
378
|
+
throw new ClaimRepairError(
|
|
379
|
+
'CLAIM_REPAIR_BACKUP_DRIFT',
|
|
380
|
+
'Backup changed while its isolated restore proof was being verified',
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return Object.freeze({
|
|
384
|
+
schema_version: 'forge.claim-repair.backup-proof.v1',
|
|
385
|
+
integrity: 'ok',
|
|
386
|
+
backup_sha256: restoredHash,
|
|
387
|
+
plan_digest: restored.digest,
|
|
388
|
+
restore_digest: restored.digest,
|
|
389
|
+
});
|
|
390
|
+
} finally {
|
|
391
|
+
if (restoreDriver && typeof restoreDriver.close === 'function') restoreDriver.close();
|
|
392
|
+
cleanupRestoreProofDirectory(restoreDir);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function createVerifiedClaimRepairBackup(options = {}) {
|
|
397
|
+
const { sourceDriver, backupPath, observedAt, openDriver, hardenPath, hardenPaths } = options;
|
|
398
|
+
if (!sourceDriver || typeof sourceDriver.preflightLegacyClaimRepair !== 'function'
|
|
399
|
+
|| typeof sourceDriver.backup !== 'function') {
|
|
400
|
+
throw new ClaimRepairError('CLAIM_REPAIR_BACKUP_UNAVAILABLE', 'Source driver cannot create a verified SQLite backup');
|
|
401
|
+
}
|
|
402
|
+
if (!backupPath) {
|
|
403
|
+
throw new ClaimRepairError('CLAIM_REPAIR_BACKUP_REQUIRED', 'A separate SQLite backup path is required');
|
|
404
|
+
}
|
|
405
|
+
if (fs.existsSync(backupPath)) {
|
|
406
|
+
throw new ClaimRepairError(
|
|
407
|
+
'CLAIM_REPAIR_BACKUP_EXISTS',
|
|
408
|
+
'Refusing to overwrite an existing claim-repair backup; choose a new immutable path',
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const source = await sourceDriver.preflightLegacyClaimRepair({ observedAt });
|
|
412
|
+
try {
|
|
413
|
+
await sourceDriver.backup(backupPath, {}, { noReplace: true });
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error?.code === 'EEXIST') {
|
|
416
|
+
throw new ClaimRepairError(
|
|
417
|
+
'CLAIM_REPAIR_BACKUP_EXISTS',
|
|
418
|
+
'Refusing to overwrite a claim-repair backup path created during publication',
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
const proof = await verifyClaimRepairBackup({ backupPath, observedAt, openDriver, hardenPath, hardenPaths });
|
|
424
|
+
if (proof.restore_digest !== source.digest) {
|
|
425
|
+
throw new ClaimRepairError(
|
|
426
|
+
'CLAIM_REPAIR_BACKUP_DRIFT',
|
|
427
|
+
'Backup does not restore to the exact preflight snapshot digest',
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
return Object.freeze({ ...proof, plan_digest: source.digest });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = {
|
|
434
|
+
CLAIM_STATES,
|
|
435
|
+
ClaimRepairError,
|
|
436
|
+
buildClaimRepairPlan,
|
|
437
|
+
cleanupRestoreProofDirectory,
|
|
438
|
+
createVerifiedClaimRepairBackup,
|
|
439
|
+
prepareRestoreProofSnapshot,
|
|
440
|
+
publicClaimRepairPreflight,
|
|
441
|
+
verifyClaimRepairBackup,
|
|
442
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isLeaseExpired, isValidExpiresAt } = require('./lease-enforcer');
|
|
4
|
+
const { isTerminalStatus } = require('./taxonomy-validator');
|
|
5
|
+
|
|
6
|
+
// One authority definition for every user-facing lease projection. Null expiry is
|
|
7
|
+
// the compatibility form for a durable lease; malformed expiry cannot prove liveness.
|
|
8
|
+
function isLiveClaim(claim, issue, now) {
|
|
9
|
+
if (!claim || !issue) return false;
|
|
10
|
+
if (claim.state !== 'active') return false;
|
|
11
|
+
if (isTerminalStatus(issue.status)) return false;
|
|
12
|
+
if (!isValidExpiresAt(claim.expires_at)) return false;
|
|
13
|
+
return !isLeaseExpired(claim, now);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function projectLiveClaims(claims, issues, now) {
|
|
17
|
+
const claimRows = claims ?? [];
|
|
18
|
+
const issueRows = issues ?? [];
|
|
19
|
+
const issuesById = new Map(issueRows.map(issue => [issue.id, issue]));
|
|
20
|
+
return claimRows.filter(claim => isLiveClaim(claim, issuesById.get(claim.issue_id), now));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
isLiveClaim,
|
|
25
|
+
projectLiveClaims,
|
|
26
|
+
};
|
package/lib/kernel/migrations.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { getKernelSchema, validateKernelSchema } = require('./schema');
|
|
2
|
+
const { USAGE_EVIDENCE_MIGRATION } = require('../../packages/memory');
|
|
2
3
|
|
|
3
4
|
function assertIdentifier(identifier, label) {
|
|
4
5
|
if (!/^[a-z][a-z0-9_]*$/.test(identifier)) {
|
|
@@ -24,12 +25,21 @@ function renderColumn(field) {
|
|
|
24
25
|
if (field.primaryKey) parts.push('PRIMARY KEY');
|
|
25
26
|
if (field.default !== undefined) parts.push(`DEFAULT ${field.default}`);
|
|
26
27
|
if (field.references) parts.push(renderReference(field.references));
|
|
28
|
+
if (field.check) parts.push(`CHECK (${field.check})`);
|
|
27
29
|
return parts.join(' ');
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
function renderCreateTable(table) {
|
|
31
33
|
assertIdentifier(table.sqlName, 'table');
|
|
32
|
-
const
|
|
34
|
+
const definitions = table.fields.map(field => ` ${renderColumn(field)}`);
|
|
35
|
+
if (table.primaryKey) {
|
|
36
|
+
const columns = table.primaryKey.map(column => {
|
|
37
|
+
assertIdentifier(column, 'primary key column');
|
|
38
|
+
return column;
|
|
39
|
+
}).join(', ');
|
|
40
|
+
definitions.push(` PRIMARY KEY (${columns})`);
|
|
41
|
+
}
|
|
42
|
+
const columns = definitions.join(',\n');
|
|
33
43
|
return `CREATE TABLE IF NOT EXISTS ${table.sqlName} (\n${columns}\n);`;
|
|
34
44
|
}
|
|
35
45
|
|
|
@@ -57,8 +67,13 @@ function renderDropTable(table) {
|
|
|
57
67
|
// stays the full current schema; the named tables are filtered out of 001 so they are
|
|
58
68
|
// created exactly once by their dedicated migration (both on a fresh DB and, via the
|
|
59
69
|
// ledger, on an existing DB). KEEP IN SYNC with every new table-creating migration.
|
|
60
|
-
// memories → 005 ; pr → 009
|
|
61
|
-
const MIGRATION_ADDED_TABLES = [
|
|
70
|
+
// memories → 005 ; pr → 009 ; PR watch ownership → 012
|
|
71
|
+
const MIGRATION_ADDED_TABLES = [
|
|
72
|
+
'memories',
|
|
73
|
+
'pr',
|
|
74
|
+
'pr_watch_owners',
|
|
75
|
+
'pr_watch_migration_gate',
|
|
76
|
+
];
|
|
62
77
|
|
|
63
78
|
function getInitialKernelSchema() {
|
|
64
79
|
const schema = getKernelSchema();
|
|
@@ -304,6 +319,100 @@ function buildPrLinkageMigration() {
|
|
|
304
319
|
};
|
|
305
320
|
}
|
|
306
321
|
|
|
322
|
+
// 010: Memory-owned durable monitor evidence. This is intentionally additive and
|
|
323
|
+
// isolated from Kernel authority tables. Rollback is non-destructive: it turns off
|
|
324
|
+
// new monitor writes while retaining every event, outbox row, receipt, and cursor.
|
|
325
|
+
function buildMonitorDurabilityMigration() {
|
|
326
|
+
return {
|
|
327
|
+
id: '010_memory_monitor_durability',
|
|
328
|
+
apply: [
|
|
329
|
+
`CREATE TABLE IF NOT EXISTS memory_monitor_writer_state (
|
|
330
|
+
singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1),
|
|
331
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1))
|
|
332
|
+
);`,
|
|
333
|
+
'INSERT OR IGNORE INTO memory_monitor_writer_state (singleton, enabled) VALUES (1, 1);',
|
|
334
|
+
`CREATE TABLE IF NOT EXISTS memory_monitor_events (
|
|
335
|
+
event_id TEXT NOT NULL PRIMARY KEY,
|
|
336
|
+
monitor_id TEXT NOT NULL,
|
|
337
|
+
sequence INTEGER NOT NULL,
|
|
338
|
+
content_hash TEXT NOT NULL,
|
|
339
|
+
envelope_json TEXT NOT NULL,
|
|
340
|
+
artifact_digest TEXT,
|
|
341
|
+
created_at TEXT NOT NULL,
|
|
342
|
+
UNIQUE (monitor_id, sequence)
|
|
343
|
+
);`,
|
|
344
|
+
`CREATE TABLE IF NOT EXISTS memory_monitor_outbox (
|
|
345
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
346
|
+
event_id TEXT NOT NULL REFERENCES memory_monitor_events(event_id),
|
|
347
|
+
target TEXT NOT NULL,
|
|
348
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
349
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
350
|
+
next_attempt_at TEXT,
|
|
351
|
+
created_at TEXT NOT NULL,
|
|
352
|
+
UNIQUE (event_id, target)
|
|
353
|
+
);`,
|
|
354
|
+
`CREATE TABLE IF NOT EXISTS memory_monitor_delivery_receipts (
|
|
355
|
+
event_id TEXT NOT NULL REFERENCES memory_monitor_events(event_id),
|
|
356
|
+
target TEXT NOT NULL,
|
|
357
|
+
attempt INTEGER NOT NULL,
|
|
358
|
+
content_hash TEXT NOT NULL,
|
|
359
|
+
envelope_json TEXT NOT NULL,
|
|
360
|
+
acknowledged INTEGER NOT NULL,
|
|
361
|
+
delivered_at TEXT NOT NULL,
|
|
362
|
+
outcome TEXT NOT NULL,
|
|
363
|
+
PRIMARY KEY (event_id, target, attempt)
|
|
364
|
+
);`,
|
|
365
|
+
`CREATE TABLE IF NOT EXISTS memory_monitor_cursors (
|
|
366
|
+
monitor_id TEXT NOT NULL,
|
|
367
|
+
target TEXT NOT NULL,
|
|
368
|
+
sequence INTEGER NOT NULL,
|
|
369
|
+
updated_at TEXT NOT NULL,
|
|
370
|
+
PRIMARY KEY (monitor_id, target)
|
|
371
|
+
);`,
|
|
372
|
+
`CREATE TABLE IF NOT EXISTS memory_monitor_receipts (
|
|
373
|
+
monitor_id TEXT NOT NULL PRIMARY KEY,
|
|
374
|
+
content_hash TEXT NOT NULL,
|
|
375
|
+
envelope_json TEXT NOT NULL,
|
|
376
|
+
owner_run_id TEXT NOT NULL,
|
|
377
|
+
terminal_state TEXT NOT NULL,
|
|
378
|
+
last_sequence INTEGER NOT NULL,
|
|
379
|
+
evidence_digest TEXT NOT NULL,
|
|
380
|
+
undelivered_cursor INTEGER,
|
|
381
|
+
created_at TEXT NOT NULL
|
|
382
|
+
);`,
|
|
383
|
+
'CREATE INDEX IF NOT EXISTS idx_memory_monitor_events_monitor_sequence ON memory_monitor_events (monitor_id, sequence);',
|
|
384
|
+
'CREATE INDEX IF NOT EXISTS idx_memory_monitor_outbox_pending ON memory_monitor_outbox (target, status, next_attempt_at, created_at);',
|
|
385
|
+
'CREATE INDEX IF NOT EXISTS idx_memory_monitor_delivery_receipts_event_target_attempt ON memory_monitor_delivery_receipts (event_id, target, attempt);',
|
|
386
|
+
],
|
|
387
|
+
rollback: [
|
|
388
|
+
'UPDATE memory_monitor_writer_state SET enabled = 0 WHERE singleton = 1;',
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 011: usage evidence is append-only and rollback-retained. The public Memory
|
|
394
|
+
// contract owns the DDL; Kernel only places it in its ledgered migration sequence.
|
|
395
|
+
function buildUsageEvidenceMigration() {
|
|
396
|
+
return {
|
|
397
|
+
id: USAGE_EVIDENCE_MIGRATION.id,
|
|
398
|
+
apply: [...USAGE_EVIDENCE_MIGRATION.apply],
|
|
399
|
+
rollback: [...USAGE_EVIDENCE_MIGRATION.rollback],
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function buildPrWatchOwnershipMigration() {
|
|
404
|
+
const ownerTableNames = new Set(['pr_watch_owners', 'pr_watch_migration_gate']);
|
|
405
|
+
const tables = getKernelSchema().tables.filter(table => ownerTableNames.has(table.name));
|
|
406
|
+
const migration = buildSchemaMigration({ version: 1, tables });
|
|
407
|
+
return {
|
|
408
|
+
id: '012_kernel_pr_watch_ownership',
|
|
409
|
+
apply: migration.apply,
|
|
410
|
+
rollback: [
|
|
411
|
+
"INSERT INTO kernel_pr_watch_migration_gate (singleton, state, snapshot_hash, conflict_code, updated_at) VALUES (1, 'quarantined', NULL, NULL, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ON CONFLICT(singleton) DO UPDATE SET state = 'quarantined', conflict_code = NULL, updated_at = excluded.updated_at;",
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
307
416
|
function validateKernelMigrations(migrations) {
|
|
308
417
|
const ids = new Set();
|
|
309
418
|
for (const migration of migrations) {
|
|
@@ -335,6 +444,9 @@ function buildKernelMigrationPlan(migrations = [
|
|
|
335
444
|
buildWorktreeLinkageColumnsMigration(),
|
|
336
445
|
buildMemoryFtsMigration(),
|
|
337
446
|
buildPrLinkageMigration(),
|
|
447
|
+
buildMonitorDurabilityMigration(),
|
|
448
|
+
buildUsageEvidenceMigration(),
|
|
449
|
+
buildPrWatchOwnershipMigration(),
|
|
338
450
|
]) {
|
|
339
451
|
validateKernelMigrations(migrations);
|
|
340
452
|
|
|
@@ -353,6 +465,9 @@ module.exports = {
|
|
|
353
465
|
buildKernelMigrationPlan,
|
|
354
466
|
buildMemoryFtsMigration,
|
|
355
467
|
buildMemoryProjectionMigration,
|
|
468
|
+
buildMonitorDurabilityMigration,
|
|
469
|
+
buildPrWatchOwnershipMigration,
|
|
470
|
+
buildUsageEvidenceMigration,
|
|
356
471
|
buildPrLinkageMigration,
|
|
357
472
|
buildSchemaMigration,
|
|
358
473
|
buildWorktreeLinkageColumnsMigration,
|