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.
Files changed (196) hide show
  1. package/AGENTS.md +18 -7
  2. package/CHANGELOG.md +79 -1
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/README.md +6 -2
  6. package/bin/forge-cmd.js +20 -0
  7. package/bin/forge.js +28 -375
  8. package/docs/INDEX.md +1 -1
  9. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  10. package/docs/guides/MIGRATION.md +4 -4
  11. package/docs/guides/SETUP.md +16 -16
  12. package/docs/reference/COMMANDS.md +8 -5
  13. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  14. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  15. package/docs/reference/INSTALL.md +4 -0
  16. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  17. package/docs/reference/RELEASE.md +5 -3
  18. package/docs/reference/TOOLCHAIN.md +8 -0
  19. package/docs/reference/github-accounts.md +134 -0
  20. package/docs/reference/protected-state-surfaces.md +4 -4
  21. package/docs/reference/shepherd.md +114 -35
  22. package/lefthook.yml +12 -0
  23. package/lib/activation/ensure-forge-home.js +33 -15
  24. package/lib/adapters/pr-state-adapter.js +359 -144
  25. package/lib/audit-evidence.js +71 -110
  26. package/lib/base-remote.js +138 -0
  27. package/lib/beta5-compatibility-evidence.js +1093 -0
  28. package/lib/bun-lockfile-proof.js +413 -0
  29. package/lib/bun-workflow-pins.js +461 -0
  30. package/lib/capabilities/index.js +9 -0
  31. package/lib/capabilities/model.js +141 -0
  32. package/lib/capabilities/probes.js +347 -0
  33. package/lib/capped-jsonl-log.js +236 -0
  34. package/lib/codex-skills.js +2 -2
  35. package/lib/commands/_manifest.js +1 -0
  36. package/lib/commands/_registry.js +50 -20
  37. package/lib/commands/clean.js +252 -32
  38. package/lib/commands/dev.js +4 -33
  39. package/lib/commands/doctor.js +37 -6
  40. package/lib/commands/gate.js +197 -27
  41. package/lib/commands/github.js +215 -0
  42. package/lib/commands/hooks.js +276 -30
  43. package/lib/commands/insights.js +8 -3
  44. package/lib/commands/memory.js +66 -2
  45. package/lib/commands/merge.js +1265 -58
  46. package/lib/commands/plan.js +33 -2
  47. package/lib/commands/pr.js +3 -1
  48. package/lib/commands/preflight.js +21 -4
  49. package/lib/commands/prime.js +21 -8
  50. package/lib/commands/push.js +146 -54
  51. package/lib/commands/recall.js +127 -49
  52. package/lib/commands/recap.js +6 -1
  53. package/lib/commands/release.js +39 -3
  54. package/lib/commands/remember.js +28 -4
  55. package/lib/commands/serve.js +26 -9
  56. package/lib/commands/setup.js +323 -98
  57. package/lib/commands/shepherd.js +591 -73
  58. package/lib/commands/ship.js +36 -91
  59. package/lib/commands/skill.js +127 -11
  60. package/lib/commands/status.js +17 -1
  61. package/lib/commands/team.js +47 -8
  62. package/lib/commands/test.js +187 -38
  63. package/lib/commands/validate.js +65 -21
  64. package/lib/commands/worktree.js +359 -45
  65. package/lib/core/runtime-graph.js +1 -1
  66. package/lib/doc-assertions.js +297 -0
  67. package/lib/existing-tdd-gate.js +253 -0
  68. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  69. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  70. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  71. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  72. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  73. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  74. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  75. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  76. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  77. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  78. package/lib/forge-context.js +1 -4
  79. package/lib/forge-issues.js +134 -32
  80. package/lib/gate-events.js +98 -10
  81. package/lib/git-defaults.js +56 -0
  82. package/lib/github-context.js +308 -0
  83. package/lib/global-flags.js +1 -0
  84. package/lib/harness-capability-matrix.js +3 -3
  85. package/lib/hook-renderer.js +122 -5
  86. package/lib/insights.js +96 -80
  87. package/lib/issue-render.js +19 -0
  88. package/lib/kernel/backing-issue.js +14 -2
  89. package/lib/kernel/broker.js +739 -31
  90. package/lib/kernel/claim-reconciler.js +238 -0
  91. package/lib/kernel/cli-broker-factory.js +12 -1
  92. package/lib/kernel/close-on-merge.js +154 -0
  93. package/lib/kernel/fs-class.js +42 -25
  94. package/lib/kernel/lease-enforcer.js +9 -4
  95. package/lib/kernel/legacy-claim-repair.js +442 -0
  96. package/lib/kernel/live-claim-projection.js +26 -0
  97. package/lib/kernel/migrations.js +118 -3
  98. package/lib/kernel/readiness-model.js +184 -12
  99. package/lib/kernel/schema.js +49 -1
  100. package/lib/kernel/sqlite-driver.js +3435 -172
  101. package/lib/kernel/taxonomy-validator.js +4 -1
  102. package/lib/kernel/windows-private-acl.js +239 -0
  103. package/lib/lefthook-wiring.js +21 -1
  104. package/lib/memory/hygiene.js +191 -0
  105. package/lib/memory/router.js +110 -28
  106. package/lib/memory/usage-evidence.js +4 -0
  107. package/lib/memory-digest.js +106 -15
  108. package/lib/memory-recall-events.js +145 -0
  109. package/lib/memory-recall.js +71 -10
  110. package/lib/merge-rules.js +143 -21
  111. package/lib/npm-publish-workflow.js +465 -0
  112. package/lib/orientation.js +68 -43
  113. package/lib/package-root.js +2 -0
  114. package/lib/plugin-catalog.js +14 -4
  115. package/lib/pr-bundle.js +5 -6
  116. package/lib/pr-monitor/auto-actions.js +169 -28
  117. package/lib/pr-monitor/differ.js +110 -4
  118. package/lib/pr-monitor/events.js +0 -0
  119. package/lib/pr-monitor/flow-monitor.js +1424 -0
  120. package/lib/pr-monitor/gather.js +251 -44
  121. package/lib/pr-monitor/journal.js +18 -39
  122. package/lib/pr-monitor/monitor.js +117 -10
  123. package/lib/pr-monitor/process-identity.js +117 -0
  124. package/lib/pr-monitor/reconcile-executor.js +1129 -470
  125. package/lib/pr-monitor/reconcile.js +0 -0
  126. package/lib/pr-monitor/render-summary.js +293 -0
  127. package/lib/pr-monitor/review-preflight.js +269 -0
  128. package/lib/pr-monitor/shepherd-lease.js +38 -20
  129. package/lib/pr-monitor/verdict.js +438 -0
  130. package/lib/pr-monitor/watch-lifecycle.js +145 -27
  131. package/lib/pr-monitor/watch-owner.js +1414 -0
  132. package/lib/pr-monitor/watch.js +129 -58
  133. package/lib/pr-pull.js +33 -14
  134. package/lib/pr-shepherd.js +51 -11
  135. package/lib/preflight/gates.js +65 -18
  136. package/lib/preflight/runner.js +5 -0
  137. package/lib/project-memory.js +178 -4
  138. package/lib/protected-state-authority.js +1100 -0
  139. package/lib/protected-state-surfaces.js +243 -45
  140. package/lib/release-readiness.js +53 -7
  141. package/lib/review-adapter.js +65 -0
  142. package/lib/shell-utils.js +1 -1
  143. package/lib/skills-sync.js +71 -35
  144. package/lib/smart-merge.js +28 -4
  145. package/lib/symlink-utils.js +74 -26
  146. package/lib/upgrade-safety.js +39 -0
  147. package/lib/using-forge.js +19 -6
  148. package/lib/validation/risk-manifest.js +339 -0
  149. package/lib/workflow/enforce-stage.js +44 -0
  150. package/lib/workflow/plan-authority.js +225 -0
  151. package/package.json +12 -9
  152. package/scripts/commitlint.js +13 -15
  153. package/scripts/doc-asserting-tests.js +158 -0
  154. package/scripts/generate-risk-manifest.js +91 -0
  155. package/scripts/github-context-bridge.sh +10 -0
  156. package/scripts/legacy-claim-repair.js +145 -0
  157. package/scripts/lib/behavioral-eval-runner.js +310 -0
  158. package/scripts/lib/behavioral-eval-runtime.js +457 -0
  159. package/scripts/lib/eval-evidence.js +328 -0
  160. package/scripts/lib/eval-runner.js +81 -41
  161. package/scripts/lib/immutable-eval-corpus.js +309 -0
  162. package/scripts/lib/promotion-evidence-loader.js +94 -0
  163. package/scripts/lib/promotion-scorecard.js +314 -0
  164. package/scripts/npm-release-receipt.js +134 -0
  165. package/scripts/process-tree.js +773 -0
  166. package/scripts/protected-state-check.js +479 -31
  167. package/scripts/run-command-eval.js +29 -1
  168. package/scripts/sync-agent-skills.js +333 -34
  169. package/scripts/sync-d20-audit.js +172 -0
  170. package/scripts/test-full-suite.js +935 -37
  171. package/scripts/test-profile.js +13 -3
  172. package/scripts/test.js +271 -57
  173. package/skills/coverage.json +1 -0
  174. package/skills/review/SKILL.md +6 -11
  175. package/skills/review/evals/scorecard.json +4 -4
  176. package/skills/rollback/SKILL.md +4 -11
  177. package/skills/rollback/evals/scorecard.json +3 -3
  178. package/skills/setup/SKILL.md +18 -0
  179. package/skills/setup/evals/scorecard.json +3 -3
  180. package/skills/shepherd/SKILL.md +39 -16
  181. package/skills/shepherd/evals/scorecard.json +4 -4
  182. package/skills/ship/SKILL.md +4 -12
  183. package/skills/ship/evals/scorecard.json +3 -3
  184. package/skills/validate/SKILL.md +3 -0
  185. package/skills/validate/evals/scorecard.json +1 -1
  186. package/skills/worktree/SKILL.md +6 -1
  187. package/skills/worktree/evals/scorecard.json +2 -2
  188. package/lib/beads-setup.js +0 -538
  189. package/lib/beads-sync-scaffold.js +0 -189
  190. package/lib/pat-setup.js +0 -207
  191. package/lib/pr-monitor/render-sticky.js +0 -206
  192. package/lib/pr-monitor/upsert-sticky.js +0 -169
  193. package/scripts/beads-context.sh +0 -577
  194. package/scripts/beads-migrate-to-dolt.sh +0 -7
  195. package/scripts/beads-upgrade-smoke.sh +0 -284
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -0,0 +1,1414 @@
1
+ 'use strict';
2
+
3
+ const journal = require('./journal');
4
+ const { processIdentityAlive } = require('./process-identity');
5
+
6
+ const VERSION = 1;
7
+ const MAX_REPO_BYTES = 256;
8
+ const MAX_GENERATION_BYTES = 128;
9
+ const MAX_RECEIPT_BYTES = 256;
10
+ const REPOSITORY = /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/;
11
+ const SHA256 = /^[0-9a-f]{64}$/;
12
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
13
+ const PHASES = new Set(['starting', 'running', 'stop_requested', 'terminal_pending', 'complete', 'blocked']);
14
+ const BLOCK_REASONS = new Set([
15
+ 'legacy_live_pid',
16
+ 'legacy_conflict',
17
+ 'legacy_unreadable',
18
+ 'legacy_lossy',
19
+ 'legacy_receipt_unverified',
20
+ ]);
21
+ const CONFLICT_CODES = new Set([
22
+ 'legacy_identity_unmappable',
23
+ 'legacy_snapshot_changed',
24
+ 'legacy_owner_conflict',
25
+ ]);
26
+ const GATE_STATES = new Set(['quarantined', 'conflict', 'complete']);
27
+ const MONOTONIC_OWNER_METHODS = new Set([
28
+ 'watchOwnerReserveReopened', 'watchOwnerBindRunning', 'watchOwnerHeartbeat',
29
+ 'watchOwnerRequestStop', 'watchOwnerRecordTerminal', 'watchOwnerCompleteTerminal',
30
+ 'watchOwnerRecoverDeadStarting', 'watchOwnerRecoverDeadWatcher',
31
+ 'watchOwnerMarkLegacyBlocked', 'watchOwnerRecheckLegacyBlocked',
32
+ 'watchOwnerImportLegacyStarting', 'watchOwnerImportLegacyComplete',
33
+ ]);
34
+
35
+ const MUTATION_INPUT_FIELDS = [
36
+ 'generation', 'controllerPid', 'pid', 'recoveryControllerPid',
37
+ 'expectedReceiptId', 'providerEvidence', 'terminalReceiptId',
38
+ 'blockReason', 'legacyEvidenceHash', 'snapshotHash', 'legacyPid',
39
+ 'action', 'updatedAt', 'startedAt', 'conflictCode', 'pidReuseProven',
40
+ 'expectedSnapshotHash', 'expectedConflictCode', 'replacementSnapshotHash',
41
+ ];
42
+ const CAPTURED_INPUT = Symbol('capturedMutationInput');
43
+ const BOUND_OPTIONS = new WeakSet();
44
+ // Mirror of the builtin driver's enumeration caps (lib/kernel/sqlite-driver.js
45
+ // WATCH_OWNER_ENUMERATION_LIMIT/BYTES) so alternate adapters cannot allocate
46
+ // without bound through the public enumerateOwners API.
47
+ const OWNER_ENUMERATION_LIMIT = 4_096;
48
+ const OWNER_ENUMERATION_BYTES = 4 * 1024 * 1024;
49
+
50
+ function utf8Length(value) {
51
+ return typeof value === 'string' ? Buffer.byteLength(value, 'utf8') : -1;
52
+ }
53
+
54
+ function positivePid(value) {
55
+ return Number.isSafeInteger(value) && value > 0;
56
+ }
57
+
58
+ function canonicalTimestamp(value) {
59
+ if (typeof value !== 'string' || utf8Length(value) !== 24 || !TIMESTAMP.test(value)) return false;
60
+ try {
61
+ return new Date(value).toISOString() === value;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function boundedString(value, maximum) {
68
+ const bytes = utf8Length(value);
69
+ return bytes > 0 && bytes <= maximum;
70
+ }
71
+
72
+ function validHash(value) {
73
+ return typeof value === 'string' && SHA256.test(value);
74
+ }
75
+
76
+ function normalizeIdentity(ctx) {
77
+ try {
78
+ const repoValue = ctx?.repo;
79
+ const pr = ctx?.pr;
80
+ const repo = typeof repoValue === 'string' ? repoValue.toLowerCase() : '';
81
+ if (!REPOSITORY.test(repo) || utf8Length(repo) > MAX_REPO_BYTES
82
+ || !Number.isSafeInteger(pr) || pr <= 0) return null;
83
+ return { repo, pr };
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ // Read the caller's identity exactly once per operation and freeze it: an
90
+ // accessor-backed (or clock-mutated) ctx must not present repository A to the
91
+ // mutation and repository B to the CAS evidence read.
92
+ function captureIdentity(ctx) {
93
+ const identity = normalizeIdentity(ctx);
94
+ return identity ? Object.freeze(identity) : null;
95
+ }
96
+
97
+ function nowIso(opts = {}, fallback) {
98
+ try {
99
+ const value = fallback !== undefined
100
+ ? fallback
101
+ : (opts.now !== undefined
102
+ ? (typeof opts.now === 'function' ? opts.now() : opts.now)
103
+ : new Date().toISOString());
104
+ return canonicalTimestamp(value) ? value : null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ function fromSqliteOwnerRow(row) {
111
+ if (!row) return null;
112
+ return {
113
+ repo: row.repo,
114
+ pr: row.pr,
115
+ version: row.version,
116
+ generation: row.generation,
117
+ phase: row.phase,
118
+ controllerPid: row.controller_pid,
119
+ watcherPid: row.watcher_pid,
120
+ startedAt: row.started_at,
121
+ updatedAt: row.updated_at,
122
+ heartbeatAt: row.heartbeat_at,
123
+ terminalReceiptId: row.terminal_receipt_id,
124
+ blockReason: row.block_reason,
125
+ legacyEvidenceHash: row.legacy_evidence_hash,
126
+ };
127
+ }
128
+
129
+ function captureRecord(record) {
130
+ try {
131
+ return {
132
+ repo: record.repo,
133
+ pr: record.pr,
134
+ version: record.version,
135
+ generation: record.generation,
136
+ phase: record.phase,
137
+ controllerPid: record.controllerPid,
138
+ watcherPid: record.watcherPid,
139
+ startedAt: record.startedAt,
140
+ updatedAt: record.updatedAt,
141
+ heartbeatAt: record.heartbeatAt,
142
+ terminalReceiptId: record.terminalReceiptId,
143
+ blockReason: record.blockReason,
144
+ legacyEvidenceHash: record.legacyEvidenceHash,
145
+ };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ function validateRecord(record) {
152
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return 'invalid_identity';
153
+ const value = captureRecord(record);
154
+ if (!value) return 'invalid_record';
155
+ const identity = normalizeIdentity(value);
156
+ if (!identity || value.repo !== identity.repo || value.version !== VERSION) return 'invalid_identity';
157
+ if (!boundedString(value.generation, MAX_GENERATION_BYTES) || !PHASES.has(value.phase)) return 'invalid_generation_or_phase';
158
+ if (!canonicalTimestamp(value.startedAt) || !canonicalTimestamp(value.updatedAt)
159
+ || value.updatedAt < value.startedAt) return 'invalid_timestamp';
160
+ if (value.heartbeatAt != null && !canonicalTimestamp(value.heartbeatAt)) return 'invalid_heartbeat';
161
+ if (['running', 'stop_requested', 'terminal_pending'].includes(value.phase)
162
+ && value.heartbeatAt != null
163
+ && (value.heartbeatAt < value.startedAt || value.heartbeatAt > value.updatedAt)) return 'invalid_heartbeat';
164
+ if (value.terminalReceiptId != null && !boundedString(value.terminalReceiptId, MAX_RECEIPT_BYTES)) return 'invalid_receipt';
165
+ if (value.legacyEvidenceHash != null && !validHash(value.legacyEvidenceHash)) return 'invalid_evidence';
166
+ if (value.controllerPid != null && !positivePid(value.controllerPid)) return 'invalid_controller_pid';
167
+ if (value.watcherPid != null && !positivePid(value.watcherPid)) return 'invalid_watcher_pid';
168
+ if (value.phase === 'starting') {
169
+ return positivePid(value.controllerPid) && value.watcherPid == null && value.heartbeatAt == null
170
+ && value.terminalReceiptId == null && value.blockReason == null
171
+ ? null : 'invalid_starting';
172
+ }
173
+ if (value.phase === 'running' || value.phase === 'stop_requested') {
174
+ return value.controllerPid == null && positivePid(value.watcherPid) && value.heartbeatAt != null
175
+ && value.terminalReceiptId == null && value.blockReason == null
176
+ ? null : 'invalid_active';
177
+ }
178
+ if (value.phase === 'terminal_pending') {
179
+ return value.controllerPid == null && positivePid(value.watcherPid) && value.heartbeatAt != null
180
+ && value.terminalReceiptId != null && value.blockReason == null ? null : 'invalid_terminal_pending';
181
+ }
182
+ if (value.phase === 'complete') {
183
+ return value.controllerPid == null && value.watcherPid == null && value.heartbeatAt == null
184
+ && value.terminalReceiptId != null && value.blockReason == null ? null : 'invalid_complete';
185
+ }
186
+ if (!BLOCK_REASONS.has(value.blockReason) || value.controllerPid != null || value.heartbeatAt != null
187
+ || !validHash(value.legacyEvidenceHash)) return 'invalid_blocked';
188
+ return (value.blockReason === 'legacy_live_pid') === positivePid(value.watcherPid)
189
+ ? null : 'invalid_blocked_pid';
190
+ }
191
+
192
+ function copyMigrationGate(gate) {
193
+ try {
194
+ if (!gate || typeof gate !== 'object' || Array.isArray(gate)) return null;
195
+ const copy = {
196
+ singleton: gate.singleton,
197
+ state: gate.state,
198
+ snapshot_hash: gate.snapshot_hash,
199
+ conflict_code: gate.conflict_code,
200
+ updated_at: gate.updated_at,
201
+ };
202
+ if (copy.singleton !== 1 || !GATE_STATES.has(copy.state)
203
+ || !canonicalTimestamp(copy.updated_at)) return null;
204
+ if (copy.state === 'quarantined') {
205
+ if (copy.snapshot_hash != null && !validHash(copy.snapshot_hash)) return null;
206
+ if (copy.conflict_code != null) return null;
207
+ } else {
208
+ if (!validHash(copy.snapshot_hash)) return null;
209
+ if (copy.state === 'complete'
210
+ ? copy.conflict_code != null
211
+ : !CONFLICT_CODES.has(copy.conflict_code)) return null;
212
+ }
213
+ return copy;
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+
219
+ function sameMigrationGate(left, right) {
220
+ if (left == null || right == null) return left == null && right == null;
221
+ return ['singleton', 'state', 'snapshot_hash', 'conflict_code', 'updated_at']
222
+ .every(field => Object.is(left[field], right[field]));
223
+ }
224
+
225
+ function gateMatchesMutation(method, input, gate, result) {
226
+ // Every changed gate mutation stamps the durable checkpoint; only exact
227
+ // idempotent replays may keep the prior timestamp.
228
+ const advanced = result.changed === false
229
+ ? gate.updated_at <= input.now
230
+ : gate.updated_at === input.now;
231
+ const prior = input.expectedGate;
232
+ if (result.reason === 'idempotent' && !sameMigrationGate(gate, prior)) return false;
233
+ switch (method) {
234
+ case 'watchGatePublishQuarantine':
235
+ return gate.state === 'quarantined' && gate.conflict_code == null && advanced
236
+ && (result.reason === 'idempotent' ? prior?.state === 'quarantined' : prior == null);
237
+ case 'watchGateBindSnapshot':
238
+ return gate.state === 'quarantined' && gate.snapshot_hash === input.snapshotHash
239
+ && gate.conflict_code == null && advanced && prior?.state === 'quarantined'
240
+ && (result.reason === 'idempotent'
241
+ ? prior.snapshot_hash === input.snapshotHash
242
+ : prior.snapshot_hash == null);
243
+ case 'watchGatePublishConflict':
244
+ return gate.state === 'conflict'
245
+ && gate.snapshot_hash === input.snapshotHash
246
+ && gate.conflict_code === input.conflictCode && advanced
247
+ && (result.reason === 'idempotent'
248
+ ? prior?.state === 'conflict'
249
+ : prior?.state === 'quarantined' && prior.conflict_code == null
250
+ && (prior.snapshot_hash == null || prior.snapshot_hash === input.snapshotHash));
251
+ case 'watchGateRetryConflict':
252
+ return gate.state === 'quarantined'
253
+ && gate.snapshot_hash === input.replacementSnapshotHash
254
+ && gate.conflict_code == null && advanced
255
+ && prior?.state === 'conflict'
256
+ && prior.snapshot_hash === input.expectedSnapshotHash
257
+ && prior.conflict_code === input.expectedConflictCode;
258
+ case 'watchGateCompleteMigration':
259
+ return gate.state === 'complete' && gate.snapshot_hash === input.snapshotHash && gate.conflict_code == null
260
+ && advanced && (result.reason === 'idempotent'
261
+ ? prior?.state === 'complete'
262
+ : prior?.state === 'quarantined' && prior.snapshot_hash === input.snapshotHash
263
+ && prior.conflict_code == null);
264
+ default:
265
+ return true;
266
+ }
267
+ }
268
+
269
+ function envelope(ok, changed, reason, record = null) {
270
+ return { ok, changed, reason, record };
271
+ }
272
+
273
+ function invalid(reason = 'invalid_input') {
274
+ return envelope(false, false, reason);
275
+ }
276
+
277
+ function invalidGate(reason = 'invalid_input') {
278
+ return { ok: false, changed: false, reason, gate: null };
279
+ }
280
+
281
+ function matchesIdentity(record, identity) {
282
+ return record?.repo === identity.repo && record?.pr === identity.pr;
283
+ }
284
+
285
+ function matchesOwnerRecord(record, fields, phases) {
286
+ if (!record || (phases && ![].concat(phases).includes(record.phase))) return false;
287
+ return Object.entries(fields).every(([field, expected]) => Object.is(record[field], expected));
288
+ }
289
+
290
+ function matchesOwnerSnapshot(record, snapshot) {
291
+ const expected = fromSqliteOwnerRow(snapshot);
292
+ return expected != null && matchesOwnerRecord(record, expected);
293
+ }
294
+
295
+ function validOwnerSuccess(result, record, reason, fields, phases, allowIdempotent = false) {
296
+ const idempotent = result.reason === 'idempotent';
297
+ if ((!idempotent && result.reason !== reason) || (idempotent && !allowIdempotent)
298
+ || result.changed !== !idempotent) return false;
299
+ return matchesOwnerRecord(record, fields, phases);
300
+ }
301
+
302
+ // Operations that rebuild the row mint a fresh generation; a success that kept
303
+ // the prior generation would be an un-recovered/un-reopened row, not a new one.
304
+ function mintedOwnerGeneration(input, record) {
305
+ const priorGeneration = input.expectedSnapshot?.generation;
306
+ if (priorGeneration == null) return false;
307
+ return record != null && !Object.is(record.generation, priorGeneration);
308
+ }
309
+
310
+ // Import inserts mint a fresh generation while its idempotent replay keeps the
311
+ // original row, so the required relation to the prior snapshot flips by path.
312
+ // An idempotent replay without a captured prior row is impossible: replaying
313
+ // requires that a row was previously imported. expectedSnapshot fields are the
314
+ // raw snake_case driver row (deliberate).
315
+ function importedOwnerGeneration(result, input, record) {
316
+ const priorGeneration = input.expectedSnapshot?.generation;
317
+ if (!record) return false;
318
+ if (result.reason === 'idempotent') {
319
+ return priorGeneration != null && Object.is(record.generation, priorGeneration);
320
+ }
321
+ // A changed insert is only legal over an ABSENT owner; the builtin
322
+ // transaction answers any existing row with owner_conflict/idempotent.
323
+ return priorGeneration == null;
324
+ }
325
+
326
+ // A transitioned row must carry the submitted timestamp — an unchanged row
327
+ // returned as changed would fake durable-progress advancement. Idempotent
328
+ // replays legitimately keep their original timestamps.
329
+ function stampedOwnerTimestamp(result, input, record, field) {
330
+ if (result.reason === 'idempotent') return true;
331
+ return record != null && Object.is(record[field], input.now);
332
+ }
333
+
334
+ function validOwnerOperationResult(method, input, result, record) {
335
+ if (method === 'watchOwnerRead') {
336
+ return result.changed === false
337
+ && (result.reason === 'read') === (record != null)
338
+ && (record != null || result.reason === 'absent');
339
+ }
340
+ if (result.reason === 'idempotent'
341
+ && (!matchesOwnerSnapshot(record, input.expectedSnapshot)
342
+ || [record.updatedAt, record.heartbeatAt]
343
+ .filter(Boolean)
344
+ .some(timestamp => timestamp > input.now))) return false;
345
+ switch (method) {
346
+ case 'watchOwnerReserveStarting':
347
+ return input.expectedSnapshot === null && validOwnerSuccess(result, record, 'acquired', {
348
+ controllerPid: input.controllerPid, legacyEvidenceHash: null,
349
+ }, 'starting')
350
+ && stampedOwnerTimestamp(result, input, record, 'startedAt')
351
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt');
352
+ case 'watchOwnerReserveReopened': {
353
+ const prior = input.expectedSnapshot;
354
+ if (!prior) return false;
355
+ return validOwnerSuccess(result, record, 'reopened', {
356
+ controllerPid: input.controllerPid,
357
+ legacyEvidenceHash: prior.legacy_evidence_hash,
358
+ }, 'starting') && mintedOwnerGeneration(input, record)
359
+ && stampedOwnerTimestamp(result, input, record, 'startedAt')
360
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt');
361
+ }
362
+ case 'watchOwnerBindRunning': {
363
+ // Imported provenance and the original started_at survive the bind.
364
+ const prior = input.expectedSnapshot;
365
+ if (!prior || prior.legacy_evidence_hash === undefined
366
+ || prior.started_at === undefined) return false;
367
+ if ((prior.phase === 'running') !== (result.reason === 'idempotent')) return false;
368
+ return validOwnerSuccess(result, record, 'bound', {
369
+ generation: input.generation, watcherPid: input.watcherPid,
370
+ }, 'running', true)
371
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt')
372
+ && stampedOwnerTimestamp(result, input, record, 'heartbeatAt')
373
+ && Object.is(record.legacyEvidenceHash, prior.legacy_evidence_hash)
374
+ && Object.is(record.startedAt, prior.started_at);
375
+ }
376
+ case 'watchOwnerHeartbeat': {
377
+ const prior = input.expectedSnapshot;
378
+ if (!prior || (prior.phase !== 'running' && prior.phase !== 'stop_requested')) return false;
379
+ // Imported provenance and the original started_at survive the beat.
380
+ if (prior.legacy_evidence_hash === undefined || prior.started_at === undefined) return false;
381
+ return validOwnerSuccess(result, record, 'heartbeat', {
382
+ generation: input.generation, watcherPid: input.watcherPid,
383
+ updatedAt: input.now, heartbeatAt: input.now,
384
+ legacyEvidenceHash: prior.legacy_evidence_hash,
385
+ startedAt: prior.started_at,
386
+ }, [prior.phase]);
387
+ }
388
+ case 'watchOwnerRequestStop': {
389
+ const prior = input.expectedSnapshot;
390
+ if (!prior || !['running', 'stop_requested'].includes(prior.phase)) return false;
391
+ if ((prior.phase === 'stop_requested') !== (result.reason === 'idempotent')) return false;
392
+ return validOwnerSuccess(result, record, 'stop_requested', {
393
+ generation: input.generation, watcherPid: input.watcherPid,
394
+ heartbeatAt: prior.heartbeat_at,
395
+ legacyEvidenceHash: prior.legacy_evidence_hash, startedAt: prior.started_at,
396
+ }, 'stop_requested', true) && stampedOwnerTimestamp(result, input, record, 'updatedAt');
397
+ }
398
+ case 'watchOwnerRecordTerminal': {
399
+ const prior = input.expectedSnapshot;
400
+ if (!prior || !['running', 'stop_requested', 'terminal_pending'].includes(prior.phase)) return false;
401
+ if ((prior.phase === 'terminal_pending') !== (result.reason === 'idempotent')) return false;
402
+ return validOwnerSuccess(result, record, 'terminal_pending', {
403
+ generation: input.generation, watcherPid: input.watcherPid,
404
+ terminalReceiptId: input.terminalReceiptId,
405
+ heartbeatAt: prior.heartbeat_at,
406
+ legacyEvidenceHash: prior.legacy_evidence_hash, startedAt: prior.started_at,
407
+ }, 'terminal_pending', true) && stampedOwnerTimestamp(result, input, record, 'updatedAt');
408
+ }
409
+ case 'watchOwnerCompleteTerminal': {
410
+ const prior = input.expectedSnapshot;
411
+ if (!prior || !['terminal_pending', 'complete'].includes(prior.phase)) return false;
412
+ if ((prior.phase === 'complete') !== (result.reason === 'idempotent')) return false;
413
+ return validOwnerSuccess(result, record, 'complete', {
414
+ generation: input.generation, watcherPid: null,
415
+ terminalReceiptId: input.terminalReceiptId,
416
+ legacyEvidenceHash: prior.legacy_evidence_hash, startedAt: prior.started_at,
417
+ }, 'complete', true) && stampedOwnerTimestamp(result, input, record, 'updatedAt');
418
+ }
419
+ case 'watchOwnerAbortStarting': {
420
+ if (!input.expectedSnapshot) return false;
421
+ return result.reason === 'aborted' && result.changed === true && record === null;
422
+ }
423
+ case 'watchOwnerReleaseNonterminal': {
424
+ const prior = input.expectedSnapshot;
425
+ return prior?.phase === 'stop_requested'
426
+ && prior.generation === input.generation
427
+ && prior.watcher_pid === input.watcherPid
428
+ && result.reason === 'released' && result.changed === true && record === null;
429
+ }
430
+ case 'watchOwnerRecoverDeadStarting': {
431
+ const prior = input.expectedSnapshot;
432
+ if (!prior) return false;
433
+ return validOwnerSuccess(result, record, 'recovered', {
434
+ controllerPid: input.controllerPid,
435
+ legacyEvidenceHash: prior.legacy_evidence_hash,
436
+ }, 'starting') && mintedOwnerGeneration(input, record)
437
+ && stampedOwnerTimestamp(result, input, record, 'startedAt')
438
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt');
439
+ }
440
+ case 'watchOwnerRecoverDeadWatcher': {
441
+ const prior = input.expectedSnapshot;
442
+ if (!prior || !['running', 'stop_requested'].includes(prior.phase)) return false;
443
+ return validOwnerSuccess(result, record, 'recovered', {
444
+ controllerPid: input.controllerPid,
445
+ legacyEvidenceHash: prior.legacy_evidence_hash,
446
+ }, 'starting') && mintedOwnerGeneration(input, record)
447
+ && stampedOwnerTimestamp(result, input, record, 'startedAt')
448
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt');
449
+ }
450
+ case 'watchOwnerMarkLegacyBlocked':
451
+ if (!quarantinedGateMatches(input) || (input.expectedSnapshot && result.reason !== 'idempotent')) return false;
452
+ return validOwnerSuccess(result, record, 'blocked', {
453
+ watcherPid: input.watcherPid, terminalReceiptId: input.terminalReceiptId ?? null,
454
+ blockReason: input.blockReason, legacyEvidenceHash: input.legacyEvidenceHash,
455
+ }, 'blocked', true)
456
+ && stampedOwnerTimestamp(result, input, record, 'startedAt')
457
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt');
458
+ case 'watchOwnerRecheckLegacyBlocked': {
459
+ if (input.action === 'release') {
460
+ if (input.expectedSnapshot?.block_reason !== 'legacy_live_pid') return false;
461
+ return result.reason === 'released' && result.changed === true && record === null;
462
+ }
463
+ const prior = input.expectedSnapshot;
464
+ if (!prior || prior.phase !== 'blocked') return false;
465
+ return validOwnerSuccess(result, record, 'complete', {
466
+ generation: input.generation, watcherPid: null,
467
+ terminalReceiptId: input.terminalReceiptId, blockReason: null,
468
+ legacyEvidenceHash: prior.legacy_evidence_hash, startedAt: prior.started_at,
469
+ }, 'complete') && stampedOwnerTimestamp(result, input, record, 'updatedAt');
470
+ }
471
+ case 'watchOwnerImportLegacyStarting':
472
+ return quarantinedGateMatches(input) && validOwnerSuccess(result, record, 'imported', {
473
+ controllerPid: input.controllerPid, legacyEvidenceHash: input.legacyEvidenceHash,
474
+ }, 'starting', true) && importedOwnerGeneration(result, input, record)
475
+ && stampedOwnerTimestamp(result, input, record, 'startedAt')
476
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt');
477
+ case 'watchOwnerImportLegacyComplete':
478
+ return quarantinedGateMatches(input) && validOwnerSuccess(result, record, 'imported', {
479
+ watcherPid: null, terminalReceiptId: input.terminalReceiptId,
480
+ legacyEvidenceHash: input.legacyEvidenceHash,
481
+ }, 'complete', true) && importedOwnerGeneration(result, input, record)
482
+ && stampedOwnerTimestamp(result, input, record, 'startedAt')
483
+ && stampedOwnerTimestamp(result, input, record, 'updatedAt');
484
+ default:
485
+ return false;
486
+ }
487
+ }
488
+
489
+ function quarantinedGateMatches(input) {
490
+ return input.expectedGate?.state === 'quarantined'
491
+ && input.expectedGate.snapshot_hash === input.snapshotHash
492
+ && input.expectedGate.conflict_code == null;
493
+ }
494
+
495
+ function bindAuthorityOptions(opts = {}, methods = []) {
496
+ try {
497
+ if (BOUND_OPTIONS.has(opts)) return opts;
498
+ const driver = opts?.driver;
499
+ const databaseConfig = opts?.databaseConfig || {};
500
+ const stableDatabaseConfig = databaseConfig && typeof databaseConfig === 'object'
501
+ ? Object.freeze({ ...databaseConfig })
502
+ : databaseConfig;
503
+ const authorityMethods = {};
504
+ for (const method of methods) {
505
+ const candidate = driver && typeof driver[method] === 'function' ? driver[method] : null;
506
+ if (candidate) authorityMethods[method] = candidate.bind(driver);
507
+ }
508
+ const bound = {
509
+ ...opts,
510
+ driver,
511
+ databaseConfig: stableDatabaseConfig,
512
+ authorityMethods: Object.freeze(authorityMethods),
513
+ };
514
+ BOUND_OPTIONS.add(bound);
515
+ return Object.freeze(bound);
516
+ } catch {
517
+ return Object.freeze({ authorityMethods: Object.freeze({}) });
518
+ }
519
+ }
520
+
521
+ function captureMutationInput(input = {}) {
522
+ const captured = {};
523
+ try {
524
+ const hasUpdatedAt = input != null && Object.prototype.hasOwnProperty.call(input, 'updatedAt');
525
+ const hasStartedAt = input != null && Object.prototype.hasOwnProperty.call(input, 'startedAt');
526
+ for (const field of MUTATION_INPUT_FIELDS) captured[field] = input == null ? undefined : input[field];
527
+ Object.defineProperty(captured, CAPTURED_INPUT, {
528
+ value: { hasUpdatedAt, hasStartedAt },
529
+ });
530
+ return captured;
531
+ } catch {
532
+ return null;
533
+ }
534
+ }
535
+
536
+ function invoke(method, identity, input, opts) {
537
+ const operation = opts?.authorityMethods?.[method];
538
+ if (typeof operation !== 'function') return invalid('authority_unavailable');
539
+ if (ownerMutationRegresses(method, { ...identity, ...input })) return invalid('stale_evidence');
540
+ try {
541
+ const result = operation({ ...identity, ...input }, opts.databaseConfig || {});
542
+ if (result && typeof result.then === 'function') {
543
+ // Consume the orphaned rejection so a rejecting async adapter cannot crash the host.
544
+ Promise.resolve(result).then(() => {}, () => {});
545
+ return invalid('invalid_operation');
546
+ }
547
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return invalid('corrupt');
548
+ // Snapshot once: accessor-backed results must not flip between
549
+ // validation and the returned envelope.
550
+ const snap = {
551
+ ok: result.ok === true,
552
+ changed: result.changed === true,
553
+ reason: typeof result.reason === 'string' ? result.reason : '',
554
+ row: result.row,
555
+ };
556
+ const record = fromSqliteOwnerRow(snap.row);
557
+ if (snap.row != null && (validateRecord(record) || !matchesIdentity(record, identity))) return invalid('corrupt');
558
+ if (!snap.ok && snap.changed) return invalid('corrupt');
559
+ if (snap.ok && !validOwnerOperationResult(method, { ...identity, ...input }, snap, record)) return invalid('corrupt');
560
+ return envelope(snap.ok, snap.changed, snap.reason, record);
561
+ } catch (error) {
562
+ return invalid(error?.code === 'AUTHORITY_UNAVAILABLE' ? 'authority_unavailable' : 'store_error');
563
+ }
564
+ }
565
+
566
+ function ownerMutationRegresses(method, input) {
567
+ const prior = input.expectedSnapshot;
568
+ if (!prior || !MONOTONIC_OWNER_METHODS.has(method)
569
+ || (method === 'watchOwnerRecheckLegacyBlocked' && input.action === 'release')) return false;
570
+ return [prior.updated_at, prior.heartbeat_at]
571
+ .filter(Boolean)
572
+ .some(timestamp => Date.parse(input.now) < Date.parse(timestamp));
573
+ }
574
+
575
+ function captureAuthoritySnapshot(identity, opts) {
576
+ if (!identity) return { ok: false, reason: 'invalid_input' };
577
+ const operation = opts?.authorityMethods?.watchOwnerRead;
578
+ if (typeof operation !== 'function') return { ok: false, reason: 'authority_unavailable' };
579
+ try {
580
+ const result = operation(identity, opts.databaseConfig || {});
581
+ if (result && typeof result.then === 'function') {
582
+ Promise.resolve(result).then(() => {}, () => {});
583
+ return { ok: false, reason: 'invalid_operation' };
584
+ }
585
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return { ok: false, reason: 'corrupt' };
586
+ // Snapshot once: accessor-backed results must not flip between the
587
+ // read-invariant probe and the evidence snapshot taken from them.
588
+ const snap = {
589
+ ok: result.ok === true,
590
+ changed: result.changed === true,
591
+ reason: typeof result.reason === 'string' ? result.reason : '',
592
+ row: result.row,
593
+ };
594
+ if (!snap.ok && snap.changed) return { ok: false, reason: 'corrupt' };
595
+ if (!snap.ok) return { ok: false, reason: snap.reason };
596
+ // Bind the evidence to one plain copy of the row: accessor-backed
597
+ // fields must not present different owners to the probe and the CAS
598
+ // snapshot.
599
+ let rawRow = null;
600
+ if (snap.row != null) {
601
+ if (typeof snap.row !== 'object' || Array.isArray(snap.row)) return { ok: false, reason: 'corrupt' };
602
+ rawRow = { ...snap.row };
603
+ }
604
+ const probe = rawRow && fromSqliteOwnerRow(rawRow);
605
+ if (snap.changed !== false
606
+ || (snap.reason === 'read') !== (probe != null)
607
+ || (probe == null && snap.reason !== 'absent')) {
608
+ return { ok: false, reason: 'corrupt' };
609
+ }
610
+ if (!rawRow) return { ok: true, identity, snapshot: null };
611
+ const record = fromSqliteOwnerRow(rawRow);
612
+ if (validateRecord(record) || !matchesIdentity(record, identity)) return { ok: false, reason: 'corrupt' };
613
+ return { ok: true, identity, snapshot: Object.freeze(rawRow) };
614
+ } catch (error) {
615
+ return { ok: false, reason: error?.code === 'AUTHORITY_UNAVAILABLE' ? 'authority_unavailable' : 'store_error' };
616
+ }
617
+ }
618
+
619
+ async function evidenceVerified(identity, evidence, states, opts) {
620
+ if (typeof opts.verifyProviderEvidence !== 'function') return false;
621
+ try {
622
+ return await opts.verifyProviderEvidence(evidence, { ...identity, states }) === true;
623
+ } catch {
624
+ return false;
625
+ }
626
+ }
627
+
628
+ async function receiptVerified(identity, receipt, opts) {
629
+ if (!boundedString(receipt, MAX_RECEIPT_BYTES) || typeof opts.verifyTerminalReceipt !== 'function') return false;
630
+ try {
631
+ return await opts.verifyTerminalReceipt(receipt, Object.freeze({ ...identity })) === true;
632
+ } catch {
633
+ return false;
634
+ }
635
+ }
636
+
637
+ async function pidState(pid, opts) {
638
+ const inspect = opts.isPidAlive || journal.pidAlive;
639
+ try {
640
+ return await inspect(pid);
641
+ } catch {
642
+ return null;
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Recovery normally demands a dead PID. A caller that observed PID reuse may
648
+ * instead present `pidReuseProven`, but the flag alone is never enough: this
649
+ * authority re-derives the proof from the row's own marker (the timestamp the
650
+ * owner process itself wrote) and an observed process start time. No flag, no
651
+ * marker, or no start-time probe means no proof, and a live PID still blocks
652
+ * recovery — fail closed.
653
+ *
654
+ * @param {number} pid
655
+ * @param {string} marker owner-written timestamp the process must predate
656
+ * @param {object} input captured mutation input
657
+ * @param {object} opts bound authority options
658
+ * @returns {Promise<boolean>} true when recovery may proceed
659
+ */
660
+ async function pidDeadOrProvenReused(pid, marker, input, opts) {
661
+ const state = await pidState(pid, opts);
662
+ if (state === false) return true;
663
+ if (input?.pidReuseProven !== true) return false;
664
+ try {
665
+ return await processIdentityAlive({
666
+ pid, startedAt: marker, isPidAlive: async () => state, pidStartedAt: opts.pidStartedAt,
667
+ }) === 'reused';
668
+ } catch {
669
+ return false;
670
+ }
671
+ }
672
+
673
+ function mutationInput(identity, input, opts, { generation = true, now = true } = {}) {
674
+ if (!identity) return null;
675
+ const value = { ...identity };
676
+ if (generation) {
677
+ if (!boundedString(input?.generation, MAX_GENERATION_BYTES)) return null;
678
+ value.generation = input.generation;
679
+ }
680
+ if (now) {
681
+ const captured = input?.[CAPTURED_INPUT];
682
+ const hasUpdatedAt = captured ? captured.hasUpdatedAt : input != null && Object.prototype.hasOwnProperty.call(input, 'updatedAt');
683
+ const hasStartedAt = captured ? captured.hasStartedAt : input != null && Object.prototype.hasOwnProperty.call(input, 'startedAt');
684
+ const timestamp = hasUpdatedAt ? input.updatedAt : (hasStartedAt ? input.startedAt : undefined);
685
+ value.now = hasUpdatedAt || hasStartedAt
686
+ ? (canonicalTimestamp(timestamp) ? timestamp : null)
687
+ : nowIso(opts);
688
+ if (!value.now) return null;
689
+ }
690
+ return value;
691
+ }
692
+
693
+ async function readOwner(ctx, opts = {}) {
694
+ const identity = captureIdentity(ctx);
695
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead']);
696
+ return identity ? invoke('watchOwnerRead', identity, {}, boundOpts) : invalid();
697
+ }
698
+
699
+ async function enumerateOwners(_ctx, opts = {}) {
700
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerList']);
701
+ const operation = boundOpts.authorityMethods?.watchOwnerList;
702
+ if (typeof operation !== 'function') {
703
+ return { ok: false, changed: false, reason: 'authority_unavailable', records: [] };
704
+ }
705
+ try {
706
+ const result = operation(boundOpts.databaseConfig || {});
707
+ if (result && typeof result.then === 'function') {
708
+ Promise.resolve(result).then(() => {}, () => {});
709
+ return { ok: false, changed: false, reason: 'invalid_operation', records: [] };
710
+ }
711
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
712
+ return { ok: false, changed: false, reason: 'corrupt', records: [] };
713
+ }
714
+ // Snapshot once into a bounded plain array: an accessor-backed rows
715
+ // property (or a retained Proxy array) must not report one length and
716
+ // yield different iteration contents afterwards.
717
+ const snap = {
718
+ ok: result.ok === true,
719
+ changed: result.changed === true,
720
+ reason: typeof result.reason === 'string' ? result.reason : '',
721
+ rows: result.rows,
722
+ };
723
+ if (!snap.ok && snap.changed) {
724
+ return { ok: false, changed: false, reason: 'corrupt', records: [] };
725
+ }
726
+ if (snap.ok && (snap.changed !== false || snap.reason !== 'read')) {
727
+ return { ok: false, changed: false, reason: 'corrupt', records: [] };
728
+ }
729
+ if (!Array.isArray(snap.rows) && typeof snap.rows?.[Symbol.iterator] !== 'function') {
730
+ return { ok: false, changed: false, reason: 'corrupt', records: [] };
731
+ }
732
+ if (!snap.ok) {
733
+ // Failed reads must never expose converted rows as if they were
734
+ // authoritative data.
735
+ return { ok: false, changed: false, reason: snap.reason, records: [] };
736
+ }
737
+ const rows = [];
738
+ for (const row of snap.rows) {
739
+ rows.push(row);
740
+ if (rows.length > OWNER_ENUMERATION_LIMIT) {
741
+ return { ok: false, changed: false, reason: 'enumeration_overflow', records: [] };
742
+ }
743
+ }
744
+ let bytes = 0;
745
+ let previous = null;
746
+ const records = [];
747
+ for (const row of rows) {
748
+ const record = fromSqliteOwnerRow(row);
749
+ if (validateRecord(record)) return { ok: false, changed: false, reason: 'corrupt', records: [] };
750
+ if (previous && !(record.repo > previous.repo
751
+ || (record.repo === previous.repo && record.pr > previous.pr))) {
752
+ return { ok: false, changed: false, reason: 'corrupt', records: [] };
753
+ }
754
+ previous = record;
755
+ bytes += Buffer.byteLength(JSON.stringify(record), 'utf8');
756
+ if (bytes > OWNER_ENUMERATION_BYTES) {
757
+ return { ok: false, changed: false, reason: 'enumeration_overflow', records: [] };
758
+ }
759
+ records.push(record);
760
+ }
761
+ return { ok: snap.ok, changed: false, reason: snap.reason, records };
762
+ } catch (error) {
763
+ return { ok: false, changed: false,
764
+ reason: error?.code === 'AUTHORITY_UNAVAILABLE' ? 'authority_unavailable' : 'store_error', records: [] };
765
+ }
766
+ }
767
+
768
+ async function reserveStarting(ctx, input, opts = {}) {
769
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerReserveStarting']);
770
+ const capturedInput = captureMutationInput(input);
771
+ if (!capturedInput) return invalid();
772
+ input = capturedInput;
773
+ const identity = captureIdentity(ctx);
774
+ const value = mutationInput(identity, input, boundOpts, { generation: false });
775
+ if (!value || !positivePid(input?.controllerPid)) return invalid('invalid_reservation');
776
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
777
+ if (!captured.ok) return invalid(captured.reason);
778
+ return invoke('watchOwnerReserveStarting', value, {
779
+ controllerPid: Number(input.controllerPid), expectedSnapshot: captured.snapshot,
780
+ }, boundOpts);
781
+ }
782
+
783
+ async function reserveReopened(ctx, input, opts = {}) {
784
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerReserveReopened']);
785
+ const capturedInput = captureMutationInput(input);
786
+ if (!capturedInput) return invalid();
787
+ input = capturedInput;
788
+ const identity = captureIdentity(ctx);
789
+ const value = mutationInput(identity, input, boundOpts);
790
+ const controllerPid = input?.controllerPid;
791
+ const expectedReceiptId = input?.expectedReceiptId;
792
+ const providerEvidence = input?.providerEvidence;
793
+ if (!value || !positivePid(controllerPid)
794
+ || !boundedString(expectedReceiptId, MAX_RECEIPT_BYTES)) return invalid('invalid_reservation');
795
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
796
+ if (!captured.ok) return invalid(captured.reason);
797
+ const prior = captured.snapshot;
798
+ if (!prior || prior.phase !== 'complete'
799
+ || prior.generation !== value.generation
800
+ || prior.terminal_receipt_id !== input.expectedReceiptId) {
801
+ return invalid('stale_evidence');
802
+ }
803
+ if (!await evidenceVerified(value, providerEvidence, ['open'], boundOpts)) return invalid('provider_evidence_invalid');
804
+ return invoke('watchOwnerReserveReopened', value, {
805
+ controllerPid: Number(controllerPid), expectedReceiptId,
806
+ expectedSnapshot: captured.snapshot,
807
+ }, boundOpts);
808
+ }
809
+
810
+ async function bindRunning(ctx, input, opts = {}) {
811
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerBindRunning']);
812
+ const capturedInput = captureMutationInput(input);
813
+ if (!capturedInput) return invalid();
814
+ input = capturedInput;
815
+ const identity = captureIdentity(ctx);
816
+ const value = mutationInput(identity, input, boundOpts);
817
+ if (!value || !positivePid(input?.controllerPid) || !positivePid(input?.pid)) return invalid('invalid_pid');
818
+ // Imported provenance (legacy evidence hash, started_at) must survive the
819
+ // bind, so capture the starting row first and bind the result to it.
820
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
821
+ if (!captured.ok) return invalid(captured.reason);
822
+ if (!captured.snapshot || !['starting', 'running'].includes(captured.snapshot.phase)) {
823
+ return invalid('phase_mismatch');
824
+ }
825
+ if (captured.snapshot.generation !== value.generation) return invalid('generation_mismatch');
826
+ if (captured.snapshot.phase === 'starting'
827
+ && captured.snapshot.controller_pid !== Number(input.controllerPid)) {
828
+ return invalid('controller_pid_mismatch');
829
+ }
830
+ if (captured.snapshot.phase === 'running'
831
+ && captured.snapshot.watcher_pid !== Number(input.pid)) return invalid('pid_mismatch');
832
+ return invoke('watchOwnerBindRunning', value, {
833
+ controllerPid: Number(input.controllerPid), watcherPid: Number(input.pid),
834
+ expectedSnapshot: captured.snapshot,
835
+ }, boundOpts);
836
+ }
837
+
838
+ async function heartbeat(ctx, input, opts = {}) {
839
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerHeartbeat']);
840
+ const capturedInput = captureMutationInput(input);
841
+ if (!capturedInput) return invalid();
842
+ input = capturedInput;
843
+ const identity = captureIdentity(ctx);
844
+ const value = mutationInput(identity, input, boundOpts);
845
+ if (!value || !positivePid(input?.pid)) return invalid('invalid_pid');
846
+ // The heartbeated phase must be captured before the mutation so an
847
+ // alternate driver cannot flip running↔stop_requested in its result.
848
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
849
+ if (!captured.ok) return invalid(captured.reason);
850
+ if (!captured.snapshot || !['running', 'stop_requested'].includes(captured.snapshot.phase)) {
851
+ return invalid('phase_mismatch');
852
+ }
853
+ if (captured.snapshot.generation !== value.generation) return invalid('generation_mismatch');
854
+ if (captured.snapshot.watcher_pid !== Number(input.pid)) return invalid('pid_mismatch');
855
+ return invoke('watchOwnerHeartbeat', value, {
856
+ watcherPid: Number(input.pid), expectedSnapshot: captured.snapshot,
857
+ }, boundOpts);
858
+ }
859
+
860
+ async function requestStop(ctx, input, opts = {}) {
861
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerRequestStop']);
862
+ const capturedInput = captureMutationInput(input);
863
+ if (!capturedInput) return invalid();
864
+ input = capturedInput;
865
+ const identity = captureIdentity(ctx);
866
+ const value = mutationInput(identity, input, boundOpts);
867
+ if (!value || !positivePid(input?.pid)) return invalid('invalid_pid');
868
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
869
+ if (!captured.ok) return invalid(captured.reason);
870
+ const prior = captured.snapshot;
871
+ if (!prior || !['running', 'stop_requested'].includes(prior.phase)) return invalid('phase_mismatch');
872
+ if (prior.generation !== value.generation) return invalid('generation_mismatch');
873
+ if (prior.watcher_pid !== Number(input.pid)) return invalid('pid_mismatch');
874
+ return invoke('watchOwnerRequestStop', value, {
875
+ watcherPid: Number(input.pid), expectedSnapshot: prior,
876
+ }, boundOpts);
877
+ }
878
+
879
+ async function recordTerminal(ctx, input, opts = {}) {
880
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerRecordTerminal']);
881
+ const capturedInput = captureMutationInput(input);
882
+ if (!capturedInput) return invalid();
883
+ input = capturedInput;
884
+ const identity = captureIdentity(ctx);
885
+ const value = mutationInput(identity, input, boundOpts);
886
+ const watcherPid = input?.pid;
887
+ const terminalReceiptId = input?.terminalReceiptId;
888
+ if (!value || !positivePid(watcherPid)) return invalid('invalid_pid');
889
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
890
+ if (!captured.ok) return invalid(captured.reason);
891
+ const prior = captured.snapshot;
892
+ const exactTerminalReplay = prior?.phase === 'terminal_pending'
893
+ && prior.generation === value.generation
894
+ && prior.watcher_pid === Number(watcherPid)
895
+ && prior.terminal_receipt_id === terminalReceiptId;
896
+ const boundToSubmission = prior != null
897
+ && ['running', 'stop_requested'].includes(prior.phase)
898
+ && prior.generation === value.generation
899
+ && prior.watcher_pid === Number(watcherPid);
900
+ if (!exactTerminalReplay && !boundToSubmission) return invalid('stale_evidence');
901
+ if (!exactTerminalReplay && !await receiptVerified(value, terminalReceiptId, boundOpts)) return invalid('receipt_unverified');
902
+ return invoke('watchOwnerRecordTerminal', value, {
903
+ watcherPid: Number(watcherPid), terminalReceiptId,
904
+ expectedSnapshot: captured.snapshot,
905
+ }, boundOpts);
906
+ }
907
+
908
+ async function completeTerminal(ctx, input, opts = {}) {
909
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerCompleteTerminal']);
910
+ const capturedInput = captureMutationInput(input);
911
+ if (!capturedInput) return invalid();
912
+ input = capturedInput;
913
+ const identity = captureIdentity(ctx);
914
+ const value = mutationInput(identity, input, boundOpts);
915
+ const watcherPid = input?.pid;
916
+ const terminalReceiptId = input?.terminalReceiptId;
917
+ if (!value || !positivePid(watcherPid) || !boundedString(terminalReceiptId, MAX_RECEIPT_BYTES)) return invalid();
918
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
919
+ if (!captured.ok) return invalid(captured.reason);
920
+ const prior = captured.snapshot;
921
+ const exactCompletedReplay = prior?.phase === 'complete'
922
+ && prior.generation === value.generation
923
+ && prior.terminal_receipt_id === terminalReceiptId;
924
+ const boundToSubmission = prior != null
925
+ && prior.phase === 'terminal_pending'
926
+ && prior.generation === value.generation
927
+ && prior.watcher_pid === Number(watcherPid)
928
+ && prior.terminal_receipt_id === terminalReceiptId;
929
+ if (!exactCompletedReplay && !boundToSubmission) return invalid('stale_evidence');
930
+ // A bare "the PID exists" answer keeps a terminal_pending row active forever once
931
+ // the exited watcher's number is inherited by an unrelated process: the daemon can
932
+ // never retire and a reopened PR is never rewatched. Accept the caller's reuse
933
+ // proof under the SAME re-derivation every other recovery path uses — the row's
934
+ // own watcher-written heartbeat marker.
935
+ if (!exactCompletedReplay && !await pidDeadOrProvenReused(
936
+ Number(watcherPid), prior.heartbeat_at, input, boundOpts,
937
+ )) return invalid('pid_live');
938
+ return invoke('watchOwnerCompleteTerminal', value, {
939
+ watcherPid: Number(watcherPid), terminalReceiptId,
940
+ expectedSnapshot: captured.snapshot,
941
+ }, boundOpts);
942
+ }
943
+
944
+ async function abortStarting(ctx, input, opts = {}) {
945
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerAbortStarting']);
946
+ const capturedInput = captureMutationInput(input);
947
+ if (!capturedInput) return invalid();
948
+ input = capturedInput;
949
+ const identity = captureIdentity(ctx);
950
+ const value = mutationInput(identity, input, boundOpts, { now: false });
951
+ const controllerPid = input?.controllerPid;
952
+ if (!value || !positivePid(controllerPid)) return invalid('invalid_pid');
953
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
954
+ if (!captured.ok) return invalid(captured.reason);
955
+ const prior = captured.snapshot;
956
+ if (!prior || prior.phase !== 'starting'
957
+ || prior.generation !== value.generation
958
+ || prior.controller_pid !== Number(controllerPid)) {
959
+ return invalid('stale_evidence');
960
+ }
961
+ if (await pidState(Number(controllerPid), boundOpts) !== true) return invalid('controller_dead');
962
+ return invoke('watchOwnerAbortStarting', value, {
963
+ controllerPid: Number(controllerPid), expectedSnapshot: captured.snapshot,
964
+ }, boundOpts);
965
+ }
966
+
967
+ async function releaseNonterminal(ctx, input, opts = {}) {
968
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerReleaseNonterminal']);
969
+ const capturedInput = captureMutationInput(input);
970
+ if (!capturedInput) return invalid();
971
+ input = capturedInput;
972
+ const identity = captureIdentity(ctx);
973
+ const value = mutationInput(identity, input, boundOpts, { now: false });
974
+ if (!value || !positivePid(input?.pid)) return invalid('invalid_pid');
975
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
976
+ if (!captured.ok) return invalid(captured.reason);
977
+ const prior = captured.snapshot;
978
+ if (!prior || prior.phase !== 'stop_requested') return invalid('phase_mismatch');
979
+ if (prior.generation !== value.generation) return invalid('generation_mismatch');
980
+ if (prior.watcher_pid !== Number(input.pid)) return invalid('pid_mismatch');
981
+ return invoke('watchOwnerReleaseNonterminal', value, {
982
+ watcherPid: Number(input.pid), expectedSnapshot: prior,
983
+ }, boundOpts);
984
+ }
985
+
986
+ async function recoverDeadStarting(ctx, input, opts = {}) {
987
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerRecoverDeadStarting']);
988
+ const capturedInput = captureMutationInput(input);
989
+ if (!capturedInput) return invalid();
990
+ input = capturedInput;
991
+ const identity = captureIdentity(ctx);
992
+ const value = mutationInput(identity, input, boundOpts);
993
+ const controllerPid = input?.controllerPid;
994
+ const recoveryControllerPid = input?.recoveryControllerPid;
995
+ if (!value || !positivePid(controllerPid) || !positivePid(recoveryControllerPid)) return invalid('invalid_pid');
996
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
997
+ if (!captured.ok) return invalid(captured.reason);
998
+ if (!captured.snapshot || captured.snapshot.phase !== 'starting'
999
+ || captured.snapshot.generation !== value.generation
1000
+ || captured.snapshot.controller_pid !== Number(controllerPid)) {
1001
+ return invalid('generation_mismatch');
1002
+ }
1003
+ if (!await pidDeadOrProvenReused(
1004
+ Number(controllerPid), captured.snapshot.updated_at, input, boundOpts,
1005
+ )) return invalid('pid_live');
1006
+ return invoke('watchOwnerRecoverDeadStarting', value, {
1007
+ expectedControllerPid: Number(controllerPid), controllerPid: Number(recoveryControllerPid),
1008
+ expectedSnapshot: captured.snapshot,
1009
+ }, boundOpts);
1010
+ }
1011
+
1012
+ async function recoverDeadWatcher(ctx, input, opts = {}) {
1013
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerRecoverDeadWatcher']);
1014
+ const capturedInput = captureMutationInput(input);
1015
+ if (!capturedInput) return invalid();
1016
+ input = capturedInput;
1017
+ const identity = captureIdentity(ctx);
1018
+ const value = mutationInput(identity, input, boundOpts);
1019
+ const watcherPid = input?.pid;
1020
+ const recoveryControllerPid = input?.recoveryControllerPid;
1021
+ const providerEvidence = input?.providerEvidence;
1022
+ if (!value || !positivePid(watcherPid) || !positivePid(recoveryControllerPid)) return invalid('invalid_pid');
1023
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
1024
+ if (!captured.ok) return invalid(captured.reason);
1025
+ // A watcher that exited after `requestStop()` but before `releaseNonterminal()`
1026
+ // landed leaves a dead `stop_requested` row. That row is as recoverable as a dead
1027
+ // `running` one — same generation, same watcher PID, same postconditions — so
1028
+ // refusing it would strand the PR unwatched forever.
1029
+ if (!captured.snapshot || !['running', 'stop_requested'].includes(captured.snapshot.phase)
1030
+ || captured.snapshot.generation !== value.generation
1031
+ || captured.snapshot.watcher_pid !== Number(watcherPid)) {
1032
+ return invalid('generation_mismatch');
1033
+ }
1034
+ if (!await pidDeadOrProvenReused(
1035
+ Number(watcherPid), captured.snapshot.heartbeat_at, input, boundOpts,
1036
+ )) return invalid('pid_live');
1037
+ if (!await evidenceVerified(value, providerEvidence, ['open', 'terminal'], boundOpts)) return invalid('provider_evidence_invalid');
1038
+ return invoke('watchOwnerRecoverDeadWatcher', value, {
1039
+ watcherPid: Number(watcherPid), controllerPid: Number(recoveryControllerPid),
1040
+ expectedSnapshot: captured.snapshot,
1041
+ }, boundOpts);
1042
+ }
1043
+
1044
+ async function markLegacyBlocked(ctx, input, opts = {}) {
1045
+ const boundOpts = bindAuthorityOptions(opts, [
1046
+ 'watchOwnerRead', 'watchGateRead', 'watchOwnerMarkLegacyBlocked',
1047
+ ]);
1048
+ const capturedInput = captureMutationInput(input);
1049
+ if (!capturedInput) return invalid();
1050
+ input = capturedInput;
1051
+ const identity = captureIdentity(ctx);
1052
+ const value = mutationInput(identity, input, boundOpts, { generation: false });
1053
+ const blockReason = input?.blockReason;
1054
+ const legacyEvidenceHash = input?.legacyEvidenceHash;
1055
+ const snapshotHash = input?.snapshotHash;
1056
+ const pid = input?.pid;
1057
+ const terminalReceiptId = input?.terminalReceiptId;
1058
+ if (!value || !BLOCK_REASONS.has(blockReason) || !validHash(legacyEvidenceHash)
1059
+ || !validHash(snapshotHash)) {
1060
+ return invalid('invalid_legacy_evidence');
1061
+ }
1062
+ if (pid != null && !positivePid(pid)) return invalid('invalid_legacy_evidence');
1063
+ const watcherPid = pid == null ? null : pid;
1064
+ if ((blockReason === 'legacy_live_pid') !== positivePid(watcherPid)) return invalid('invalid_legacy_evidence');
1065
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
1066
+ if (!captured.ok) return invalid(captured.reason);
1067
+ const gate = captureQuarantinedGate(snapshotHash, boundOpts);
1068
+ if (!gate.ok) return invalid(gate.reason);
1069
+ if (terminalReceiptId != null && !boundedString(terminalReceiptId, MAX_RECEIPT_BYTES)) return invalid('invalid_legacy_evidence');
1070
+ const exactBlockedReplay = captured.snapshot?.phase === 'blocked'
1071
+ && captured.snapshot.block_reason === blockReason
1072
+ && captured.snapshot.watcher_pid === watcherPid
1073
+ && captured.snapshot.terminal_receipt_id === (terminalReceiptId || null)
1074
+ && captured.snapshot.legacy_evidence_hash === legacyEvidenceHash;
1075
+ if (!exactBlockedReplay && watcherPid != null && await pidState(watcherPid, boundOpts) !== true) return invalid('pid_dead');
1076
+ return invoke('watchOwnerMarkLegacyBlocked', value, {
1077
+ watcherPid, blockReason, terminalReceiptId: terminalReceiptId || null,
1078
+ legacyEvidenceHash, snapshotHash,
1079
+ expectedSnapshot: captured.snapshot, expectedGate: gate.snapshot,
1080
+ }, boundOpts);
1081
+ }
1082
+
1083
+ async function recheckLegacyBlocked(ctx, input, opts = {}) {
1084
+ const boundOpts = bindAuthorityOptions(opts, ['watchOwnerRead', 'watchOwnerRecheckLegacyBlocked']);
1085
+ const capturedInput = captureMutationInput(input);
1086
+ if (!capturedInput) return invalid();
1087
+ input = capturedInput;
1088
+ const identity = captureIdentity(ctx);
1089
+ const value = mutationInput(identity, input, boundOpts);
1090
+ const action = input?.action;
1091
+ const legacyEvidenceHash = input?.legacyEvidenceHash;
1092
+ const pid = input?.pid;
1093
+ const terminalReceiptId = input?.terminalReceiptId;
1094
+ if (!value || !validHash(legacyEvidenceHash) || !['release', 'complete'].includes(action)) {
1095
+ return invalid('invalid_legacy_evidence');
1096
+ }
1097
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
1098
+ if (!captured.ok) return invalid(captured.reason);
1099
+ const prior = captured.snapshot;
1100
+ if (!prior || prior.phase !== 'blocked'
1101
+ || prior.generation !== value.generation
1102
+ || prior.legacy_evidence_hash !== input.legacyEvidenceHash) {
1103
+ return invalid('stale_evidence');
1104
+ }
1105
+ if (action === 'release' && prior.block_reason !== 'legacy_live_pid') {
1106
+ return invalid('invalid_transition');
1107
+ }
1108
+ const watcherPid = pid == null ? null : pid;
1109
+ if (prior.watcher_pid !== watcherPid) return invalid('pid_mismatch');
1110
+ // The legacy watcher never wrote a heartbeat into this row, so its identity marker
1111
+ // is the retained legacy `started_at` the import stamped. Same re-derived proof,
1112
+ // same fail-closed default: no marker or no start-time probe keeps a live PID
1113
+ // authoritative.
1114
+ if (watcherPid != null && !await pidDeadOrProvenReused(
1115
+ watcherPid, prior.started_at, input, boundOpts,
1116
+ )) return invalid('pid_live');
1117
+ if (action === 'complete' && !await receiptVerified(value, terminalReceiptId, boundOpts)) return invalid('receipt_unverified');
1118
+ return invoke('watchOwnerRecheckLegacyBlocked', value, {
1119
+ action, legacyEvidenceHash,
1120
+ terminalReceiptId: action === 'complete' ? terminalReceiptId : null,
1121
+ watcherPid, expectedSnapshot: captured.snapshot,
1122
+ }, boundOpts);
1123
+ }
1124
+
1125
+ async function importLegacyComplete(ctx, input, opts = {}) {
1126
+ const boundOpts = bindAuthorityOptions(opts, [
1127
+ 'watchOwnerRead', 'watchGateRead', 'watchOwnerImportLegacyComplete',
1128
+ ]);
1129
+ const capturedInput = captureMutationInput(input);
1130
+ if (!capturedInput) return invalid();
1131
+ input = capturedInput;
1132
+ const identity = captureIdentity(ctx);
1133
+ const value = mutationInput(identity, input, boundOpts, { generation: false });
1134
+ const snapshotHash = input?.snapshotHash;
1135
+ const legacyEvidenceHash = input?.legacyEvidenceHash;
1136
+ const legacyPid = input?.legacyPid;
1137
+ const terminalReceiptId = input?.terminalReceiptId;
1138
+ if (!value || !validHash(snapshotHash) || !validHash(legacyEvidenceHash)) {
1139
+ return invalid('invalid_legacy_evidence');
1140
+ }
1141
+ if (legacyPid != null && !positivePid(legacyPid)) return invalid('invalid_legacy_evidence');
1142
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
1143
+ if (!captured.ok) return invalid(captured.reason);
1144
+ const gate = captureQuarantinedGate(snapshotHash, boundOpts);
1145
+ if (!gate.ok) return invalid(gate.reason);
1146
+ const exactCompleteReplay = captured.snapshot?.phase === 'complete'
1147
+ && captured.snapshot.legacy_evidence_hash === legacyEvidenceHash
1148
+ && captured.snapshot.terminal_receipt_id === terminalReceiptId;
1149
+ if (!exactCompleteReplay) {
1150
+ if (legacyPid != null && await pidState(Number(legacyPid), boundOpts) !== false) return invalid('pid_live');
1151
+ if (!await receiptVerified(value, terminalReceiptId, boundOpts)) return invalid('receipt_unverified');
1152
+ }
1153
+ return invoke('watchOwnerImportLegacyComplete', value, {
1154
+ snapshotHash, legacyEvidenceHash,
1155
+ terminalReceiptId, expectedSnapshot: captured.snapshot, expectedGate: gate.snapshot,
1156
+ }, boundOpts);
1157
+ }
1158
+
1159
+ async function importLegacyStarting(ctx, input, opts = {}) {
1160
+ const boundOpts = bindAuthorityOptions(opts, [
1161
+ 'watchOwnerRead', 'watchGateRead', 'watchOwnerImportLegacyStarting',
1162
+ ]);
1163
+ const capturedInput = captureMutationInput(input);
1164
+ if (!capturedInput) return invalid();
1165
+ input = capturedInput;
1166
+ const identity = captureIdentity(ctx);
1167
+ const value = mutationInput(identity, input, boundOpts, { generation: false });
1168
+ const snapshotHash = input?.snapshotHash;
1169
+ const legacyEvidenceHash = input?.legacyEvidenceHash;
1170
+ const legacyPid = input?.legacyPid;
1171
+ const controllerPid = input?.controllerPid;
1172
+ const providerEvidence = input?.providerEvidence;
1173
+ if (!value || !validHash(snapshotHash) || !validHash(legacyEvidenceHash)
1174
+ || !positivePid(legacyPid) || !positivePid(controllerPid)) {
1175
+ return invalid('invalid_legacy_evidence');
1176
+ }
1177
+ const captured = captureAuthoritySnapshot(identity, boundOpts);
1178
+ if (!captured.ok) return invalid(captured.reason);
1179
+ const gate = captureQuarantinedGate(snapshotHash, boundOpts);
1180
+ if (!gate.ok) return invalid(gate.reason);
1181
+ const exactStartingReplay = captured.snapshot?.phase === 'starting'
1182
+ && captured.snapshot.controller_pid === controllerPid
1183
+ && captured.snapshot.legacy_evidence_hash === legacyEvidenceHash;
1184
+ if (!exactStartingReplay) {
1185
+ if (await pidState(legacyPid, boundOpts) !== false) return invalid('pid_live');
1186
+ if (!await evidenceVerified(value, providerEvidence, ['open'], boundOpts)) return invalid('provider_evidence_invalid');
1187
+ }
1188
+ return invoke('watchOwnerImportLegacyStarting', value, {
1189
+ snapshotHash, legacyEvidenceHash,
1190
+ controllerPid, expectedSnapshot: captured.snapshot, expectedGate: gate.snapshot,
1191
+ }, boundOpts);
1192
+ }
1193
+
1194
+ function gateInput(input, opts) {
1195
+ const captured = input?.[CAPTURED_INPUT];
1196
+ const hasUpdatedAt = captured ? captured.hasUpdatedAt
1197
+ : input != null && Object.prototype.hasOwnProperty.call(input, 'updatedAt');
1198
+ const now = hasUpdatedAt
1199
+ ? (canonicalTimestamp(input.updatedAt) ? input.updatedAt : null)
1200
+ : nowIso(opts);
1201
+ return now ? { now } : null;
1202
+ }
1203
+
1204
+ // Exact success envelopes the builtin gate driver can produce per method;
1205
+ // anything else is a mutating or malformed adapter response.
1206
+ const GATE_ENVELOPE_CONTRACTS = Object.freeze({
1207
+ watchGateRead: Object.freeze([Object.freeze({ changed: false, reason: 'read' })]),
1208
+ watchGatePublishQuarantine: Object.freeze([
1209
+ Object.freeze({ changed: true, reason: 'quarantined' }),
1210
+ Object.freeze({ changed: false, reason: 'idempotent' }),
1211
+ ]),
1212
+ watchGateBindSnapshot: Object.freeze([
1213
+ Object.freeze({ changed: true, reason: 'bound' }),
1214
+ Object.freeze({ changed: false, reason: 'idempotent' }),
1215
+ ]),
1216
+ watchGatePublishConflict: Object.freeze([
1217
+ Object.freeze({ changed: true, reason: 'conflict' }),
1218
+ Object.freeze({ changed: false, reason: 'idempotent' }),
1219
+ ]),
1220
+ watchGateRetryConflict: Object.freeze([
1221
+ Object.freeze({ changed: true, reason: 'retry_bound' }),
1222
+ ]),
1223
+ watchGateCompleteMigration: Object.freeze([
1224
+ Object.freeze({ changed: true, reason: 'complete' }),
1225
+ Object.freeze({ changed: false, reason: 'idempotent' }),
1226
+ ]),
1227
+ });
1228
+
1229
+ function invokeGate(method, input, opts) {
1230
+ const operation = opts?.authorityMethods?.[method];
1231
+ if (typeof operation !== 'function') return { ok: false, changed: false, reason: 'authority_unavailable', gate: null };
1232
+ if (input.expectedGate && Date.parse(input.now) < Date.parse(input.expectedGate.updated_at)) {
1233
+ return invalidGate('stale_evidence');
1234
+ }
1235
+ try {
1236
+ const expectedGate = input.expectedGate == null
1237
+ ? input.expectedGate
1238
+ : Object.freeze({ ...input.expectedGate });
1239
+ const submitted = Object.freeze({ ...input, expectedGate });
1240
+ const result = operation({ ...submitted }, opts.databaseConfig || {});
1241
+ if (result && typeof result.then === 'function') {
1242
+ Promise.resolve(result).then(() => {}, () => {});
1243
+ return invalidGate('invalid_operation');
1244
+ }
1245
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return invalidGate('corrupt');
1246
+ // Snapshot once: accessor-backed results must not flip between
1247
+ // validation and the returned envelope.
1248
+ const snap = {
1249
+ ok: result.ok === true,
1250
+ changed: result.changed === true,
1251
+ reason: typeof result.reason === 'string' ? result.reason : '',
1252
+ gate: result.gate,
1253
+ };
1254
+ if (!snap.ok && snap.changed) return invalidGate('corrupt');
1255
+ if (snap.ok) {
1256
+ const contract = GATE_ENVELOPE_CONTRACTS[method] || [];
1257
+ if (!contract.some(entry => entry.changed === snap.changed && entry.reason === snap.reason)) {
1258
+ return invalidGate('corrupt');
1259
+ }
1260
+ }
1261
+ if (method === 'watchGateRead' && snap.ok
1262
+ && (snap.changed !== false || snap.reason !== 'read')) {
1263
+ return invalidGate('corrupt');
1264
+ }
1265
+ if (method === 'watchGateRead' && !snap.ok
1266
+ && snap.reason === 'absent' && snap.gate != null) return invalidGate('corrupt');
1267
+ if (snap.ok && snap.gate == null) return invalidGate('corrupt');
1268
+ if (snap.gate == null) {
1269
+ return { ok: snap.ok, changed: snap.changed, reason: snap.reason, gate: null };
1270
+ }
1271
+ const gate = copyMigrationGate(snap.gate);
1272
+ if (!gate || (snap.ok && !gateMatchesMutation(method, submitted, gate, snap))) return invalidGate('corrupt');
1273
+ return { ok: snap.ok, changed: snap.changed, reason: snap.reason, gate };
1274
+ } catch (error) {
1275
+ return { ok: false, changed: false,
1276
+ reason: error?.code === 'AUTHORITY_UNAVAILABLE' ? 'authority_unavailable' : 'store_error', gate: null };
1277
+ }
1278
+ }
1279
+
1280
+ function captureMigrationGate(opts) {
1281
+ const captured = invokeGate('watchGateRead', {}, opts);
1282
+ if (captured.ok) return { ok: true, snapshot: Object.freeze({ ...captured.gate }) };
1283
+ return captured.reason === 'absent'
1284
+ ? { ok: true, snapshot: null }
1285
+ : { ok: false, reason: captured.reason };
1286
+ }
1287
+
1288
+ function captureQuarantinedGate(snapshotHash, opts) {
1289
+ const captured = captureMigrationGate(opts);
1290
+ if (!captured.ok) {
1291
+ return { ok: false, reason: ['corrupt', 'authority_unavailable'].includes(captured.reason)
1292
+ ? captured.reason : 'gate_mismatch' };
1293
+ }
1294
+ return captured.snapshot?.state === 'quarantined'
1295
+ && captured.snapshot.snapshot_hash === snapshotHash
1296
+ && captured.snapshot.conflict_code == null
1297
+ ? captured
1298
+ : { ok: false, reason: 'gate_mismatch' };
1299
+ }
1300
+
1301
+ async function readMigrationGate(input = {}, opts = {}) {
1302
+ const boundOpts = bindAuthorityOptions(opts, ['watchGateRead']);
1303
+ const capturedInput = captureMutationInput(input);
1304
+ return capturedInput ? invokeGate('watchGateRead', capturedInput, boundOpts) : invalidGate('invalid_input');
1305
+ }
1306
+
1307
+ async function publishMigrationQuarantine(input = {}, opts = {}) {
1308
+ const boundOpts = bindAuthorityOptions(opts, ['watchGateRead', 'watchGatePublishQuarantine']);
1309
+ const capturedInput = captureMutationInput(input);
1310
+ if (!capturedInput) return invalidGate('invalid_input');
1311
+ const value = gateInput(capturedInput, boundOpts);
1312
+ if (!value) return invalidGate();
1313
+ const captured = captureMigrationGate(boundOpts);
1314
+ return captured.ok
1315
+ ? invokeGate('watchGatePublishQuarantine', { ...value, expectedGate: captured.snapshot }, boundOpts)
1316
+ : invalidGate(captured.reason);
1317
+ }
1318
+
1319
+ async function bindMigrationSnapshot(input = {}, opts = {}) {
1320
+ const boundOpts = bindAuthorityOptions(opts, ['watchGateRead', 'watchGateBindSnapshot']);
1321
+ const capturedInput = captureMutationInput(input);
1322
+ if (!capturedInput) return invalidGate('invalid_input');
1323
+ input = capturedInput;
1324
+ const value = gateInput(input, boundOpts);
1325
+ if (!value || !validHash(input.snapshotHash)) return invalidGate('invalid_snapshot');
1326
+ const captured = captureMigrationGate(boundOpts);
1327
+ return captured.ok
1328
+ ? invokeGate('watchGateBindSnapshot', {
1329
+ ...value, snapshotHash: input.snapshotHash, expectedGate: captured.snapshot,
1330
+ }, boundOpts)
1331
+ : invalidGate(captured.reason);
1332
+ }
1333
+
1334
+ async function publishMigrationConflict(input = {}, opts = {}) {
1335
+ const boundOpts = bindAuthorityOptions(opts, ['watchGateRead', 'watchGatePublishConflict']);
1336
+ const capturedInput = captureMutationInput(input);
1337
+ if (!capturedInput) return invalidGate('invalid_input');
1338
+ input = capturedInput;
1339
+ const value = gateInput(input, boundOpts);
1340
+ if (!value || !validHash(input.snapshotHash) || !CONFLICT_CODES.has(input.conflictCode)) return invalidGate('invalid_conflict');
1341
+ const captured = captureMigrationGate(boundOpts);
1342
+ return captured.ok
1343
+ ? invokeGate('watchGatePublishConflict', {
1344
+ ...value, snapshotHash: input.snapshotHash, conflictCode: input.conflictCode,
1345
+ expectedGate: captured.snapshot,
1346
+ }, boundOpts)
1347
+ : invalidGate(captured.reason);
1348
+ }
1349
+
1350
+ async function retryMigrationConflict(input = {}, opts = {}) {
1351
+ const boundOpts = bindAuthorityOptions(opts, ['watchGateRead', 'watchGateRetryConflict']);
1352
+ const capturedInput = captureMutationInput(input);
1353
+ if (!capturedInput) return invalidGate('invalid_input');
1354
+ input = capturedInput;
1355
+ const value = gateInput(input, boundOpts);
1356
+ if (!value || !validHash(input.expectedSnapshotHash)
1357
+ || !CONFLICT_CODES.has(input.expectedConflictCode)
1358
+ || !validHash(input.replacementSnapshotHash)
1359
+ || input.replacementSnapshotHash === input.expectedSnapshotHash) return invalidGate('invalid_retry');
1360
+ const captured = captureMigrationGate(boundOpts);
1361
+ return captured.ok ? invokeGate('watchGateRetryConflict', {
1362
+ ...value,
1363
+ expectedSnapshotHash: input.expectedSnapshotHash,
1364
+ expectedConflictCode: input.expectedConflictCode,
1365
+ replacementSnapshotHash: input.replacementSnapshotHash,
1366
+ expectedGate: captured.snapshot,
1367
+ }, boundOpts) : invalidGate(captured.reason);
1368
+ }
1369
+
1370
+ async function completeMigrationGate(input = {}, opts = {}) {
1371
+ const boundOpts = bindAuthorityOptions(opts, ['watchGateRead', 'watchGateCompleteMigration']);
1372
+ const capturedInput = captureMutationInput(input);
1373
+ if (!capturedInput) return invalidGate('invalid_input');
1374
+ input = capturedInput;
1375
+ const value = gateInput(input, boundOpts);
1376
+ if (!value || !validHash(input.snapshotHash)) return invalidGate('invalid_snapshot');
1377
+ const captured = captureMigrationGate(boundOpts);
1378
+ return captured.ok
1379
+ ? invokeGate('watchGateCompleteMigration', {
1380
+ ...value, snapshotHash: input.snapshotHash, expectedGate: captured.snapshot,
1381
+ }, boundOpts)
1382
+ : invalidGate(captured.reason);
1383
+ }
1384
+
1385
+ module.exports = {
1386
+ VERSION,
1387
+ PHASES,
1388
+ BLOCK_REASONS,
1389
+ CONFLICT_CODES,
1390
+ validateRecord,
1391
+ readOwner,
1392
+ enumerateOwners,
1393
+ reserveStarting,
1394
+ reserveReopened,
1395
+ bindRunning,
1396
+ heartbeat,
1397
+ requestStop,
1398
+ recordTerminal,
1399
+ completeTerminal,
1400
+ abortStarting,
1401
+ releaseNonterminal,
1402
+ recoverDeadStarting,
1403
+ recoverDeadWatcher,
1404
+ markLegacyBlocked,
1405
+ recheckLegacyBlocked,
1406
+ importLegacyStarting,
1407
+ importLegacyComplete,
1408
+ readMigrationGate,
1409
+ publishMigrationQuarantine,
1410
+ bindMigrationSnapshot,
1411
+ publishMigrationConflict,
1412
+ retryMigrationConflict,
1413
+ completeMigrationGate,
1414
+ };