codex-workflow-v2 2.0.0-beta.13.8 → 2.0.0-beta.14

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 (281) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +96 -32
  3. package/dist/src/alpha6/adoption.d.ts +2 -0
  4. package/dist/src/alpha6/adoption.js +22 -0
  5. package/dist/src/alpha6/adoption.js.map +1 -1
  6. package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
  7. package/dist/src/alpha6/captured-check-evidence.js +152 -0
  8. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  9. package/dist/src/alpha6/component-owner.d.ts +15 -1
  10. package/dist/src/alpha6/component-owner.js +44 -2
  11. package/dist/src/alpha6/component-owner.js.map +1 -1
  12. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  13. package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
  14. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  15. package/dist/src/alpha6/downstream-proof.d.ts +1 -25
  16. package/dist/src/alpha6/downstream-proof.js +0 -171
  17. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  18. package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
  19. package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
  20. package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
  21. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  22. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  23. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  24. package/dist/src/alpha6/milestone.d.ts +2 -0
  25. package/dist/src/alpha6/milestone.js +10 -1
  26. package/dist/src/alpha6/milestone.js.map +1 -1
  27. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  28. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  29. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  30. package/dist/src/alpha6/plan-integrity.js +7 -5
  31. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  32. package/dist/src/alpha6/plan-risk.d.ts +2 -0
  33. package/dist/src/alpha6/plan-risk.js +10 -3
  34. package/dist/src/alpha6/plan-risk.js.map +1 -1
  35. package/dist/src/alpha6/remediation.d.ts +6 -21
  36. package/dist/src/alpha6/remediation.js +161 -476
  37. package/dist/src/alpha6/remediation.js.map +1 -1
  38. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  39. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  40. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  41. package/dist/src/beta1/project-transaction.d.ts +3 -1
  42. package/dist/src/beta1/project-transaction.js +6 -2
  43. package/dist/src/beta1/project-transaction.js.map +1 -1
  44. package/dist/src/checks/runner.d.ts +26 -0
  45. package/dist/src/checks/runner.js +223 -0
  46. package/dist/src/checks/runner.js.map +1 -0
  47. package/dist/src/checks/task-sync.d.ts +10 -0
  48. package/dist/src/checks/task-sync.js +41 -0
  49. package/dist/src/checks/task-sync.js.map +1 -0
  50. package/dist/src/checks/worker.d.ts +1 -0
  51. package/dist/src/checks/worker.js +154 -0
  52. package/dist/src/checks/worker.js.map +1 -0
  53. package/dist/src/cli-actions.d.ts +2 -2
  54. package/dist/src/cli-actions.js +3 -15
  55. package/dist/src/cli-actions.js.map +1 -1
  56. package/dist/src/cli.js +14 -98
  57. package/dist/src/cli.js.map +1 -1
  58. package/dist/src/contracts.d.ts +35 -32
  59. package/dist/src/dependency-provenance.d.ts +2 -2
  60. package/dist/src/dependency-provenance.js +12 -69
  61. package/dist/src/dependency-provenance.js.map +1 -1
  62. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  63. package/dist/src/domain/base-sync-conflict.js +32 -0
  64. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  65. package/dist/src/domain/step-start-admission.d.ts +4 -0
  66. package/dist/src/domain/step-start-admission.js +9 -0
  67. package/dist/src/domain/step-start-admission.js.map +1 -0
  68. package/dist/src/domain/virgin-registration.d.ts +3 -0
  69. package/dist/src/domain/virgin-registration.js +65 -0
  70. package/dist/src/domain/virgin-registration.js.map +1 -0
  71. package/dist/src/errors.d.ts +1 -1
  72. package/dist/src/errors.js.map +1 -1
  73. package/dist/src/gateway-handshake.js +0 -8
  74. package/dist/src/gateway-handshake.js.map +1 -1
  75. package/dist/src/git.d.ts +3 -1
  76. package/dist/src/git.js +41 -14
  77. package/dist/src/git.js.map +1 -1
  78. package/dist/src/graph.js +25 -3
  79. package/dist/src/graph.js.map +1 -1
  80. package/dist/src/index.d.ts +1 -0
  81. package/dist/src/lifecycle/corrective-replan.js +3 -3
  82. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  83. package/dist/src/navigation-actions.d.ts +9 -0
  84. package/dist/src/navigation-actions.js +72 -0
  85. package/dist/src/navigation-actions.js.map +1 -0
  86. package/dist/src/navigation-update-artifact.d.ts +5 -0
  87. package/dist/src/navigation-update-artifact.js +227 -0
  88. package/dist/src/navigation-update-artifact.js.map +1 -0
  89. package/dist/src/navigation-update.d.ts +39 -0
  90. package/dist/src/navigation-update.js +82 -0
  91. package/dist/src/navigation-update.js.map +1 -0
  92. package/dist/src/observation.js +52 -29
  93. package/dist/src/observation.js.map +1 -1
  94. package/dist/src/observed-routes.js +5 -6
  95. package/dist/src/observed-routes.js.map +1 -1
  96. package/dist/src/pending-review-update.d.ts +0 -13
  97. package/dist/src/pending-review-update.js +1 -6
  98. package/dist/src/pending-review-update.js.map +1 -1
  99. package/dist/src/reviewer.d.ts +1 -1
  100. package/dist/src/reviewer.js +20 -17
  101. package/dist/src/reviewer.js.map +1 -1
  102. package/dist/src/runtime.d.ts +3 -0
  103. package/dist/src/runtime.js +6 -0
  104. package/dist/src/runtime.js.map +1 -0
  105. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  106. package/dist/src/state/corrective-replan-executor.js +37 -1
  107. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  108. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  109. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  110. package/dist/src/state/corrective-replan-public.js +6 -5
  111. package/dist/src/state/corrective-replan-public.js.map +1 -1
  112. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  113. package/dist/src/state/corrective-replan-transaction.js +12 -11
  114. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  115. package/dist/src/state/corrective-yield-executor.js +2 -2
  116. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  117. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  118. package/dist/src/state/corrective-yield-transaction.js +11 -10
  119. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  120. package/dist/src/state/lock.d.ts +4 -0
  121. package/dist/src/state/lock.js +21 -0
  122. package/dist/src/state/lock.js.map +1 -1
  123. package/dist/src/version.d.ts +1 -1
  124. package/dist/src/version.js +1 -1
  125. package/dist/src/version.js.map +1 -1
  126. package/dist/src/workflow-blocker-route.d.ts +8 -0
  127. package/dist/src/workflow-blocker-route.js +106 -0
  128. package/dist/src/workflow-blocker-route.js.map +1 -0
  129. package/dist/src/workflow.d.ts +48 -63
  130. package/dist/src/workflow.js +698 -1470
  131. package/dist/src/workflow.js.map +1 -1
  132. package/docs/autonomy-guardrails.md +26 -304
  133. package/docs/decisions.md +14 -112
  134. package/docs/development-flow.md +35 -238
  135. package/docs/project-memory.md +31 -50
  136. package/docs/release-app-evidence.md +190 -0
  137. package/docs/release.md +95 -363
  138. package/docs/updating-existing-project.md +19 -717
  139. package/package.json +11 -13
  140. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  141. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  142. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  143. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  144. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  145. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  146. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  147. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -740
  148. package/references/state-machine.md +4 -4
  149. package/roles/technical-planner.md +1 -1
  150. package/schemas/corrective-decision-event.schema.json +3 -1
  151. package/schemas/project-knowledge-map.schema.json +3 -1
  152. package/schemas/remediation-event.schema.json +10 -1
  153. package/schemas/task.schema.json +46 -2
  154. package/schemas/transition-payloads.schema.json +16 -1
  155. package/src/alpha6/adoption.ts +1123 -0
  156. package/src/alpha6/captured-check-evidence.ts +132 -0
  157. package/src/alpha6/check-support-anchor.ts +128 -0
  158. package/src/alpha6/component-owner.ts +168 -0
  159. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  160. package/src/alpha6/downstream-proof.ts +520 -0
  161. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  162. package/src/alpha6/handoff.ts +1443 -0
  163. package/src/alpha6/journal.ts +473 -0
  164. package/src/alpha6/literal-test-invocation.ts +84 -0
  165. package/src/alpha6/mechanical-feasibility.ts +488 -0
  166. package/src/alpha6/milestone.ts +2192 -0
  167. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  168. package/src/alpha6/npm-check-contract.ts +47 -0
  169. package/src/alpha6/plan-integrity.ts +298 -0
  170. package/src/alpha6/plan-risk.ts +1480 -0
  171. package/src/alpha6/preexecution-replan.ts +187 -0
  172. package/src/alpha6/remediation-cause.ts +98 -0
  173. package/src/alpha6/remediation.ts +2438 -0
  174. package/src/alpha6/review.ts +1198 -0
  175. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  176. package/src/alpha6/store-sidecars.ts +335 -0
  177. package/src/alpha7/autonomy.ts +411 -0
  178. package/src/alpha7/corrective-recovery.ts +1332 -0
  179. package/src/artifacts.ts +130 -0
  180. package/src/beta1/project-transaction.ts +355 -0
  181. package/src/change-explanation.ts +153 -0
  182. package/src/checks/runner.ts +245 -0
  183. package/src/checks/task-sync.ts +41 -0
  184. package/src/checks/worker.ts +149 -0
  185. package/src/cli-actions.ts +107 -0
  186. package/src/cli.ts +1457 -0
  187. package/src/contracts.ts +1494 -0
  188. package/src/credential-output.ts +89 -0
  189. package/src/credential-transport.ts +215 -0
  190. package/src/delegation.ts +190 -0
  191. package/src/dependency-provenance.ts +472 -0
  192. package/src/diagnostics.ts +93 -0
  193. package/src/domain/base-sync-conflict.ts +35 -0
  194. package/src/domain/completed-step-carryover.ts +76 -0
  195. package/src/domain/discovery.ts +27 -0
  196. package/src/domain/plan-semantics.ts +58 -0
  197. package/src/domain/step-start-admission.ts +10 -0
  198. package/src/domain/validation.ts +177 -0
  199. package/src/domain/virgin-registration.ts +46 -0
  200. package/src/errors.ts +24 -0
  201. package/src/fs-utils.ts +61 -0
  202. package/src/gateway-handshake.ts +95 -0
  203. package/src/git.ts +183 -0
  204. package/src/graph.ts +342 -0
  205. package/src/historical-step-provenance.ts +136 -0
  206. package/src/index.ts +23 -0
  207. package/src/lifecycle/canonical-hash.ts +28 -0
  208. package/src/lifecycle/catalog.ts +202 -0
  209. package/src/lifecycle/compiler-inspection.ts +29 -0
  210. package/src/lifecycle/core-static-readiness.ts +132 -0
  211. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  212. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  213. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  214. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  215. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  216. package/src/lifecycle/corrective-replan.ts +843 -0
  217. package/src/lifecycle/evaluator.ts +48 -0
  218. package/src/lifecycle/fingerprint.ts +488 -0
  219. package/src/lifecycle/immutable.ts +8 -0
  220. package/src/lifecycle/implementation-table.ts +118 -0
  221. package/src/lifecycle/index.ts +8 -0
  222. package/src/lifecycle/schema-artifact.ts +263 -0
  223. package/src/lifecycle/semantic-registry.ts +572 -0
  224. package/src/lifecycle/types.ts +838 -0
  225. package/src/memory.ts +273 -0
  226. package/src/migration.ts +161 -0
  227. package/src/navigation-actions.ts +70 -0
  228. package/src/navigation-update-artifact.ts +198 -0
  229. package/src/navigation-update.ts +121 -0
  230. package/src/observation.ts +225 -0
  231. package/src/observed-routes.ts +660 -0
  232. package/src/operational-contract.ts +125 -0
  233. package/src/pending-review-update.ts +175 -0
  234. package/src/repository.ts +99 -0
  235. package/src/reviewer.ts +1879 -0
  236. package/src/runtime.ts +6 -0
  237. package/src/state/corrective-replan-executor.ts +818 -0
  238. package/src/state/corrective-replan-public-schema.ts +83 -0
  239. package/src/state/corrective-replan-public.ts +908 -0
  240. package/src/state/corrective-replan-transaction.ts +949 -0
  241. package/src/state/corrective-yield-executor.ts +327 -0
  242. package/src/state/corrective-yield-transaction.ts +730 -0
  243. package/src/state/lock.ts +902 -0
  244. package/src/state/store.ts +567 -0
  245. package/src/transition-core.ts +330 -0
  246. package/src/ulid.ts +24 -0
  247. package/src/version.ts +2 -0
  248. package/src/workflow-blocker-route.ts +109 -0
  249. package/src/workflow.ts +10172 -0
  250. package/docs/alpha7.1-implementation-brief.md +0 -268
  251. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  252. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  253. package/docs/beta1-stabilization-brief.md +0 -165
  254. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  255. package/docs/beta13.2-signal-review-recovery.md +0 -38
  256. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  257. package/docs/change-model.md +0 -118
  258. package/docs/delegated-approval.md +0 -254
  259. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  260. package/docs/pdf/README.md +0 -24
  261. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  262. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  263. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  264. package/docs/pdf/requirements.txt +0 -1
  265. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  266. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -508
  267. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  268. package/docs/pending-review-update.md +0 -15
  269. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  270. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  271. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  272. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  273. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  274. package/docs/problem-briefs/README.md +0 -120
  275. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  276. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  277. package/docs/split-required-recovery.md +0 -47
  278. package/docs/stable-release-defect-register.md +0 -730
  279. package/docs/validation-report.md +0 -182
  280. package/scripts/generate-pdf-docs.py +0 -524
  281. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,1198 @@
1
+ import type {
2
+ ReviewInput,
3
+ ReviewStatus,
4
+ ReviewerAttestationEvent,
5
+ StepEvidence,
6
+ StepReviewEvent,
7
+ StepReviewEventType,
8
+ TaskState,
9
+ TaskStep,
10
+ } from '../contracts.js';
11
+ import { WorkflowError } from '../errors.js';
12
+ import type { FileStateStore } from '../state/store.js';
13
+ import { canonicalJsonStringify, assertHash64, sha256Hex } from './store-sidecars.js';
14
+ import { createUlid } from '../ulid.js';
15
+ import { reviewerCauseFingerprint } from './remediation-cause.js';
16
+
17
+ const STEP_REVIEW_EVENTS_SIDECAR = 'step-review-events.jsonl';
18
+ const REVIEWER_ATTESTATIONS_SIDECAR = 'reviewer-attestations.jsonl';
19
+ const GIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
20
+ const STEP_REVIEW_EVENT_TYPES = new Set<StepReviewEventType>([
21
+ 'pending',
22
+ 'review-recorded',
23
+ 'attestation-verified',
24
+ ]);
25
+ const REVIEW_DECISIONS = new Set<ReviewStatus>(['pending', 'passed', 'failed', 'unverified']);
26
+ const REVIEW_FINDING_SEVERITIES = new Set(['critical', 'major', 'minor']);
27
+
28
+ export interface StrictStepBinding {
29
+ stepId: string;
30
+ taskRevision: number;
31
+ stepDefinitionHash: string;
32
+ completionCommit: string;
33
+ checksHash: string;
34
+ planHash: string;
35
+ evidenceHash: string;
36
+ }
37
+
38
+ export interface CurrentStrictStepReviewBinding {
39
+ step: TaskStep;
40
+ evidence: StepEvidence;
41
+ binding: StrictStepBinding;
42
+ }
43
+
44
+ export type StrictStepReviewResolution = 'pending' | 'passed_verified' | 'failed' | 'unverified';
45
+
46
+ export interface CurrentStrictStepReviewCycle extends CurrentStrictStepReviewBinding {
47
+ latestMatchingEvent: StepReviewEvent | null;
48
+ pendingEvent: StepReviewEvent | null;
49
+ reviewRecordedEvent: StepReviewEvent | null;
50
+ reviewerAttestationEvent: ReviewerAttestationEvent | null;
51
+ attestationVerifiedEvent: StepReviewEvent | null;
52
+ resolution: StrictStepReviewResolution;
53
+ }
54
+
55
+ export interface StepReviewRecordInput {
56
+ review: ReviewInput;
57
+ reviewerOutputHash: string;
58
+ reviewedCommit: string;
59
+ isolationResult: 'verified' | 'unverified';
60
+ }
61
+
62
+ export interface StrictStepReviewRecordResult {
63
+ reviewRecordedEvent: StepReviewEvent;
64
+ reviewerAttestationEvent: ReviewerAttestationEvent;
65
+ attestationVerifiedEvent: StepReviewEvent | null;
66
+ resolution: StrictStepReviewResolution;
67
+ canFinalizeStep: boolean;
68
+ }
69
+
70
+ export interface ReviewRemediationRouting {
71
+ route: 'fix' | 'replan';
72
+ planConflicts: NonNullable<StepReviewEvent['planConflicts']>;
73
+ }
74
+
75
+ export function isStepReviewRequired(task: TaskState, stepId: string, reviewRequiredStepIds: readonly string[]): boolean {
76
+ return reviewRequiredStepIds.includes(stepId);
77
+ }
78
+
79
+ export function ensurePendingStrictStepReview(
80
+ store: FileStateStore,
81
+ task: TaskState,
82
+ stepId: string,
83
+ now: Date,
84
+ ): StepReviewEvent {
85
+ const current = buildCurrentStrictStepReviewCycle(store, task, stepId);
86
+ if (current.pendingEvent && current.latestMatchingEvent?.eventType === 'pending') return current.pendingEvent;
87
+ return appendStepReviewEvent(store, task, {
88
+ eventId: `SRE-${createUlid(now.getTime())}`,
89
+ recordedAt: now.toISOString(),
90
+ taskId: task.id,
91
+ taskRevision: task.revision,
92
+ stepId: current.binding.stepId,
93
+ stepDefinitionHash: current.binding.stepDefinitionHash,
94
+ completionCommit: current.binding.completionCommit,
95
+ checksHash: current.binding.checksHash,
96
+ planHash: current.binding.planHash,
97
+ evidenceHash: current.binding.evidenceHash,
98
+ eventType: 'pending',
99
+ decision: 'pending',
100
+ reviewerAttestationHash: null,
101
+ });
102
+ }
103
+
104
+ export function requireCurrentStrictStepReview(
105
+ store: FileStateStore,
106
+ task: TaskState,
107
+ stepId: string,
108
+ ): CurrentStrictStepReviewCycle {
109
+ const current = buildCurrentStrictStepReviewCycle(store, task, stepId);
110
+ if (!current.pendingEvent) {
111
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review pending event is missing for ${stepId}.`, {
112
+ taskId: task.id,
113
+ stepId,
114
+ completionCommit: current.binding.completionCommit,
115
+ });
116
+ }
117
+ if (current.latestMatchingEvent?.eventType !== 'pending') {
118
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review for ${stepId} is no longer pending.`, {
119
+ taskId: task.id,
120
+ stepId,
121
+ eventType: current.latestMatchingEvent?.eventType ?? null,
122
+ completionCommit: current.binding.completionCommit,
123
+ });
124
+ }
125
+ return current;
126
+ }
127
+
128
+ export function buildCurrentStrictStepReviewCycle(
129
+ store: FileStateStore,
130
+ task: TaskState,
131
+ stepId: string,
132
+ ): CurrentStrictStepReviewCycle {
133
+ const current = buildCurrentStrictStepBinding(task, stepId);
134
+ const stepReviewEvents = readStepReviewEvents(store, task);
135
+ const reviewerAttestations = readReviewerAttestations(store, task);
136
+ validateGlobalStrictStepReviewHistory(stepReviewEvents, reviewerAttestations);
137
+ return resolveCurrentStrictStepReviewCycle(current, stepReviewEvents, reviewerAttestations);
138
+ }
139
+
140
+ export function recordStrictStepReview(
141
+ store: FileStateStore,
142
+ task: TaskState,
143
+ stepId: string,
144
+ input: StepReviewRecordInput,
145
+ now: Date,
146
+ ): StrictStepReviewRecordResult {
147
+ let current = buildCurrentStrictStepReviewCycle(store, task, stepId);
148
+ if (!current.pendingEvent) {
149
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review pending event is missing for ${stepId}.`, {
150
+ taskId: task.id,
151
+ stepId,
152
+ completionCommit: current.binding.completionCommit,
153
+ });
154
+ }
155
+ validateStepReviewRecordInput(current, input);
156
+
157
+ const reviewRecordedEvent = current.reviewRecordedEvent
158
+ ? reuseReviewRecordedEvent(current, input)
159
+ : appendStepReviewEvent(store, task, {
160
+ eventId: `SRE-${createUlid(now.getTime())}`,
161
+ recordedAt: now.toISOString(),
162
+ taskId: task.id,
163
+ taskRevision: task.revision,
164
+ stepId: current.binding.stepId,
165
+ stepDefinitionHash: current.binding.stepDefinitionHash,
166
+ completionCommit: current.binding.completionCommit,
167
+ checksHash: current.binding.checksHash,
168
+ planHash: current.binding.planHash,
169
+ evidenceHash: current.binding.evidenceHash,
170
+ eventType: 'review-recorded',
171
+ decision: input.review.status,
172
+ reviewerAttestationHash: null,
173
+ ...reviewRemediationEventFields(input.review),
174
+ });
175
+
176
+ current = buildCurrentStrictStepReviewCycle(store, task, stepId);
177
+
178
+ const reviewerAttestationEvent = current.reviewerAttestationEvent
179
+ ? reuseReviewerAttestationEvent(current, reviewRecordedEvent, input)
180
+ : appendReviewerAttestationEvent(store, task, {
181
+ eventId: `RAT-${createUlid(now.getTime())}`,
182
+ recordedAt: now.toISOString(),
183
+ taskId: task.id,
184
+ stepId: current.binding.stepId,
185
+ reviewer: input.review.reviewer.trim(),
186
+ isolationResult: input.isolationResult,
187
+ outputHash: input.reviewerOutputHash,
188
+ reviewedCommit: input.reviewedCommit,
189
+ reviewEventHash: reviewRecordedEvent.eventHash,
190
+ planHash: current.binding.planHash,
191
+ evidenceHash: current.binding.evidenceHash,
192
+ });
193
+
194
+ current = buildCurrentStrictStepReviewCycle(store, task, stepId);
195
+
196
+ const attestationVerifiedEvent = input.isolationResult === 'verified'
197
+ ? current.attestationVerifiedEvent
198
+ ? reuseAttestationVerifiedEvent(current, reviewRecordedEvent, reviewerAttestationEvent)
199
+ : appendStepReviewEvent(store, task, {
200
+ eventId: `SRE-${createUlid(now.getTime())}`,
201
+ recordedAt: now.toISOString(),
202
+ taskId: task.id,
203
+ taskRevision: task.revision,
204
+ stepId: current.binding.stepId,
205
+ stepDefinitionHash: current.binding.stepDefinitionHash,
206
+ completionCommit: current.binding.completionCommit,
207
+ checksHash: current.binding.checksHash,
208
+ planHash: current.binding.planHash,
209
+ evidenceHash: current.binding.evidenceHash,
210
+ eventType: 'attestation-verified',
211
+ decision: reviewRecordedEvent.decision,
212
+ reviewerAttestationHash: reviewerAttestationEvent.eventHash,
213
+ })
214
+ : null;
215
+
216
+ const resolution = deriveStrictStepReviewResolution(reviewRecordedEvent, reviewerAttestationEvent, attestationVerifiedEvent);
217
+ return {
218
+ reviewRecordedEvent,
219
+ reviewerAttestationEvent,
220
+ attestationVerifiedEvent,
221
+ resolution,
222
+ canFinalizeStep: resolution === 'passed_verified',
223
+ };
224
+ }
225
+
226
+ export function readStepReviewEvents(store: FileStateStore, task: TaskState): StepReviewEvent[] {
227
+ return store.readSidecarEvents<StepReviewEvent>(
228
+ store.taskRoot(task.projectId, task.id),
229
+ STEP_REVIEW_EVENTS_SIDECAR,
230
+ {
231
+ validateEvent: (event, context) => validateStoredStepReviewEvent(event, task.id, context.file, context.line),
232
+ },
233
+ );
234
+ }
235
+
236
+ export function readReviewerAttestations(store: FileStateStore, task: TaskState): ReviewerAttestationEvent[] {
237
+ return store.readSidecarEvents<ReviewerAttestationEvent>(
238
+ store.taskRoot(task.projectId, task.id),
239
+ REVIEWER_ATTESTATIONS_SIDECAR,
240
+ {
241
+ validateEvent: (event, context) => validateStoredReviewerAttestationEvent(event, task.id, context.file, context.line),
242
+ },
243
+ );
244
+ }
245
+
246
+ export function latestFailedStepReviewRouting(
247
+ store: FileStateStore,
248
+ task: TaskState,
249
+ stepId: string,
250
+ ): ReviewRemediationRouting | null {
251
+ const event = readStepReviewEvents(store, task)
252
+ .filter((candidate) => candidate.stepId === stepId
253
+ && candidate.eventType === 'review-recorded'
254
+ && candidate.decision === 'failed')
255
+ .at(-1);
256
+ if (!event) return null;
257
+ return {
258
+ route: event.remediationRoute ?? 'fix',
259
+ planConflicts: event.planConflicts ?? [],
260
+ };
261
+ }
262
+
263
+ export function latestPassedVerifiedStepReviewEvent(
264
+ store: FileStateStore,
265
+ task: TaskState,
266
+ stepId: string,
267
+ ): StepReviewEvent | null {
268
+ const stepReviewEvents = readStepReviewEvents(store, task);
269
+ const reviewerAttestations = readReviewerAttestations(store, task);
270
+ validateGlobalStrictStepReviewHistory(stepReviewEvents, reviewerAttestations);
271
+ return stepReviewEvents.filter((event) =>
272
+ event.stepId === stepId
273
+ && event.eventType === 'attestation-verified'
274
+ && event.decision === 'passed'
275
+ ).at(-1) ?? null;
276
+ }
277
+
278
+ export function verifiedHistoricalStepReviewCommits(
279
+ store: FileStateStore,
280
+ task: TaskState,
281
+ ): string[] {
282
+ const stepReviewEvents = readStepReviewEvents(store, task);
283
+ const reviewerAttestations = readReviewerAttestations(store, task);
284
+ validateGlobalStrictStepReviewHistory(stepReviewEvents, reviewerAttestations);
285
+ return [...new Set(stepReviewEvents.filter((event) =>
286
+ event.eventType === 'attestation-verified'
287
+ && event.decision === 'passed'
288
+ ).map((event) => event.completionCommit))];
289
+ }
290
+
291
+ export function verifiedHistoricalStepReviewBindings(
292
+ store: FileStateStore,
293
+ task: TaskState,
294
+ ): Array<{ stepId: string; commitSha: string; planHash: string; recordedAt: string }> {
295
+ const stepReviewEvents = readStepReviewEvents(store, task);
296
+ const reviewerAttestations = readReviewerAttestations(store, task);
297
+ validateGlobalStrictStepReviewHistory(stepReviewEvents, reviewerAttestations);
298
+ return stepReviewEvents.filter((event) =>
299
+ event.eventType === 'attestation-verified'
300
+ && event.decision === 'passed'
301
+ ).map((event) => ({
302
+ stepId: event.stepId,
303
+ commitSha: event.completionCommit,
304
+ planHash: event.planHash,
305
+ recordedAt: event.recordedAt,
306
+ }));
307
+ }
308
+
309
+ export function countUnverifiedStrictStepReviews(
310
+ store: FileStateStore,
311
+ task: TaskState,
312
+ stepId: string,
313
+ ): number {
314
+ const current = buildCurrentStrictStepBinding(task, stepId);
315
+ return readStepReviewEvents(store, task).filter((event) =>
316
+ event.eventType === 'review-recorded'
317
+ && event.decision === 'unverified'
318
+ && event.stepId === current.binding.stepId
319
+ && event.completionCommit === current.binding.completionCommit
320
+ && event.planHash === current.binding.planHash
321
+ && event.evidenceHash === current.binding.evidenceHash
322
+ ).length;
323
+ }
324
+
325
+ export function deriveStrictStepReviewResolution(
326
+ reviewRecordedEvent: StepReviewEvent | null,
327
+ reviewerAttestationEvent: ReviewerAttestationEvent | null,
328
+ attestationVerifiedEvent: StepReviewEvent | null,
329
+ ): StrictStepReviewResolution {
330
+ if (!reviewRecordedEvent || !reviewerAttestationEvent) return 'pending';
331
+ if (reviewRecordedEvent.decision === 'failed') return 'failed';
332
+ if (reviewRecordedEvent.decision === 'unverified') return 'unverified';
333
+ if (reviewRecordedEvent.decision !== 'passed') return 'pending';
334
+ if (reviewerAttestationEvent.isolationResult !== 'verified') return 'unverified';
335
+ return attestationVerifiedEvent ? 'passed_verified' : 'pending';
336
+ }
337
+
338
+ function buildCurrentStrictStepBinding(task: TaskState, stepId: string): CurrentStrictStepReviewBinding {
339
+ const step = requireTaskStep(task, stepId);
340
+ if (!step.evidence) {
341
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review requires canonical evidence for ${stepId}.`, {
342
+ taskId: task.id,
343
+ stepId,
344
+ });
345
+ }
346
+ if (!task.planHash) {
347
+ throw new WorkflowError('STATE_CORRUPT', `Task ${task.id} has no current plan hash for strict Step Review.`, {
348
+ taskId: task.id,
349
+ stepId,
350
+ });
351
+ }
352
+ return {
353
+ step,
354
+ evidence: step.evidence,
355
+ binding: {
356
+ stepId: step.id,
357
+ taskRevision: task.revision,
358
+ stepDefinitionHash: sha256Hex(canonicalJsonStringify(stripStepRuntime(step))),
359
+ completionCommit: step.evidence.commitSha,
360
+ checksHash: sha256Hex(canonicalJsonStringify(step.evidence.checks)),
361
+ planHash: task.planHash,
362
+ evidenceHash: sha256Hex(canonicalJsonStringify(step.evidence)),
363
+ },
364
+ };
365
+ }
366
+
367
+ function resolveCurrentStrictStepReviewCycle(
368
+ current: CurrentStrictStepReviewBinding,
369
+ stepReviewEvents: readonly StepReviewEvent[],
370
+ reviewerAttestations: readonly ReviewerAttestationEvent[],
371
+ ): CurrentStrictStepReviewCycle {
372
+ const matchingEvents = stepReviewEvents.filter((event) => eventMatchesBinding(event, current.binding));
373
+ const latestMatchingEvent = matchingEvents.at(-1) ?? null;
374
+ const latestPendingIndex = findLastIndex(matchingEvents, (event) => event.eventType === 'pending');
375
+ if (latestPendingIndex < 0) {
376
+ if (matchingEvents.length > 0) {
377
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review history is missing the pending event that should anchor the current cycle.', {
378
+ stepId: current.binding.stepId,
379
+ completionCommit: current.binding.completionCommit,
380
+ latestEventType: latestMatchingEvent?.eventType ?? null,
381
+ });
382
+ }
383
+ return {
384
+ ...current,
385
+ latestMatchingEvent: null,
386
+ pendingEvent: null,
387
+ reviewRecordedEvent: null,
388
+ reviewerAttestationEvent: null,
389
+ attestationVerifiedEvent: null,
390
+ resolution: 'pending',
391
+ };
392
+ }
393
+
394
+ const pendingEvent = matchingEvents[latestPendingIndex]!;
395
+ const cycleEvents = matchingEvents.slice(latestPendingIndex + 1);
396
+ const reviewRecordedEvents = cycleEvents.filter((event) => event.eventType === 'review-recorded');
397
+ const attestationVerifiedEvents = cycleEvents.filter((event) => event.eventType === 'attestation-verified');
398
+
399
+ if (reviewRecordedEvents.length > 1) {
400
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review cycle has multiple review-recorded events for one pending event.', {
401
+ stepId: current.binding.stepId,
402
+ completionCommit: current.binding.completionCommit,
403
+ eventIds: reviewRecordedEvents.map((event) => event.eventId),
404
+ });
405
+ }
406
+ if (attestationVerifiedEvents.length > 1) {
407
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review cycle has multiple attestation-verified events for one pending event.', {
408
+ stepId: current.binding.stepId,
409
+ completionCommit: current.binding.completionCommit,
410
+ eventIds: attestationVerifiedEvents.map((event) => event.eventId),
411
+ });
412
+ }
413
+
414
+ const reviewRecordedEvent = reviewRecordedEvents[0] ?? null;
415
+ const attestationVerifiedEvent = attestationVerifiedEvents[0] ?? null;
416
+ let reviewerAttestationEvent: ReviewerAttestationEvent | null = null;
417
+
418
+ if (reviewRecordedEvent) {
419
+ const linkedAttestations = reviewerAttestations.filter((event) => event.reviewEventHash === reviewRecordedEvent.eventHash);
420
+ if (linkedAttestations.length > 1) {
421
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review cycle has multiple reviewer attestations for one review event.', {
422
+ stepId: current.binding.stepId,
423
+ reviewEventId: reviewRecordedEvent.eventId,
424
+ eventIds: linkedAttestations.map((event) => event.eventId),
425
+ });
426
+ }
427
+ reviewerAttestationEvent = linkedAttestations[0] ?? null;
428
+ if (reviewerAttestationEvent) validateAttestationBinding(current.binding, reviewRecordedEvent, reviewerAttestationEvent);
429
+ } else if (attestationVerifiedEvent) {
430
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification cannot exist without a review-recorded event.', {
431
+ stepId: current.binding.stepId,
432
+ eventId: attestationVerifiedEvent.eventId,
433
+ });
434
+ }
435
+
436
+ if (attestationVerifiedEvent) {
437
+ if (!reviewerAttestationEvent || !reviewRecordedEvent) {
438
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification is missing its prerequisite chain.', {
439
+ stepId: current.binding.stepId,
440
+ eventId: attestationVerifiedEvent.eventId,
441
+ });
442
+ }
443
+ validateAttestationVerifiedChain(current.binding, reviewRecordedEvent, reviewerAttestationEvent, attestationVerifiedEvent);
444
+ }
445
+
446
+ return {
447
+ ...current,
448
+ latestMatchingEvent,
449
+ pendingEvent,
450
+ reviewRecordedEvent,
451
+ reviewerAttestationEvent,
452
+ attestationVerifiedEvent,
453
+ resolution: deriveStrictStepReviewResolution(reviewRecordedEvent, reviewerAttestationEvent, attestationVerifiedEvent),
454
+ };
455
+ }
456
+
457
+ function validateGlobalStrictStepReviewHistory(
458
+ stepReviewEvents: readonly StepReviewEvent[],
459
+ reviewerAttestations: readonly ReviewerAttestationEvent[],
460
+ ): void {
461
+ const reviewRecordedEventsByHash = indexStepReviewEventsByHash(stepReviewEvents, 'review-recorded');
462
+ const reviewerAttestationsByReviewHash = new Map<string, ReviewerAttestationEvent[]>();
463
+ for (const attestation of reviewerAttestations) {
464
+ const linkedReviewEvents = reviewRecordedEventsByHash.get(attestation.reviewEventHash) ?? [];
465
+ if (linkedReviewEvents.length !== 1) {
466
+ throw new WorkflowError(
467
+ 'STATE_CORRUPT',
468
+ linkedReviewEvents.length === 0
469
+ ? 'Reviewer attestation references a missing review-recorded Step Review event.'
470
+ : 'Reviewer attestation references multiple review-recorded Step Review events.',
471
+ {
472
+ eventId: attestation.eventId,
473
+ reviewEventHash: attestation.reviewEventHash,
474
+ matchCount: linkedReviewEvents.length,
475
+ },
476
+ );
477
+ }
478
+ const linkedReviewEvent = linkedReviewEvents[0]!;
479
+ validateHistoricalAttestationBinding(linkedReviewEvent, attestation);
480
+ const linkedAttestations = reviewerAttestationsByReviewHash.get(attestation.reviewEventHash) ?? [];
481
+ linkedAttestations.push(attestation);
482
+ reviewerAttestationsByReviewHash.set(attestation.reviewEventHash, linkedAttestations);
483
+ }
484
+ for (const [reviewEventHash, linkedAttestations] of reviewerAttestationsByReviewHash) {
485
+ if (linkedAttestations.length > 1) {
486
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple reviewer attestations target the same review-recorded Step Review event.', {
487
+ reviewEventHash,
488
+ eventIds: linkedAttestations.map((event) => event.eventId),
489
+ });
490
+ }
491
+ }
492
+
493
+ const reviewerAttestationsByHash = indexReviewerAttestationsByHash(reviewerAttestations);
494
+ const attestationVerifiedEventsByAttestationHash = new Map<string, StepReviewEvent[]>();
495
+ for (const verifiedEvent of stepReviewEvents) {
496
+ if (verifiedEvent.eventType !== 'attestation-verified') continue;
497
+ const attestationHash = verifiedEvent.reviewerAttestationHash;
498
+ if (typeof attestationHash !== 'string') {
499
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event is missing its attestation hash.', {
500
+ eventId: verifiedEvent.eventId,
501
+ });
502
+ }
503
+ const linkedAttestations = reviewerAttestationsByHash.get(attestationHash) ?? [];
504
+ if (linkedAttestations.length !== 1) {
505
+ throw new WorkflowError(
506
+ 'STATE_CORRUPT',
507
+ linkedAttestations.length === 0
508
+ ? 'Attestation-verified Step Review event references a missing reviewer attestation.'
509
+ : 'Attestation-verified Step Review event references multiple reviewer attestations.',
510
+ {
511
+ eventId: verifiedEvent.eventId,
512
+ reviewerAttestationHash: attestationHash,
513
+ matchCount: linkedAttestations.length,
514
+ },
515
+ );
516
+ }
517
+ const reviewerAttestationEvent = linkedAttestations[0]!;
518
+ const linkedReviewEvents = reviewRecordedEventsByHash.get(reviewerAttestationEvent.reviewEventHash) ?? [];
519
+ if (linkedReviewEvents.length !== 1) {
520
+ throw new WorkflowError(
521
+ 'STATE_CORRUPT',
522
+ linkedReviewEvents.length === 0
523
+ ? 'Attestation-verified Step Review event references an attestation whose review-recorded event is missing.'
524
+ : 'Attestation-verified Step Review event references an attestation with multiple review-recorded targets.',
525
+ {
526
+ eventId: verifiedEvent.eventId,
527
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
528
+ reviewEventHash: reviewerAttestationEvent.reviewEventHash,
529
+ matchCount: linkedReviewEvents.length,
530
+ },
531
+ );
532
+ }
533
+ const linkedReviewEvent = linkedReviewEvents[0]!;
534
+ validateHistoricalAttestationBinding(linkedReviewEvent, reviewerAttestationEvent);
535
+ validateHistoricalAttestationVerifiedChain(linkedReviewEvent, reviewerAttestationEvent, verifiedEvent);
536
+ const linkedVerifiedEvents = attestationVerifiedEventsByAttestationHash.get(attestationHash) ?? [];
537
+ linkedVerifiedEvents.push(verifiedEvent);
538
+ attestationVerifiedEventsByAttestationHash.set(attestationHash, linkedVerifiedEvents);
539
+ }
540
+ for (const [reviewerAttestationHash, linkedVerifiedEvents] of attestationVerifiedEventsByAttestationHash) {
541
+ if (linkedVerifiedEvents.length > 1) {
542
+ throw new WorkflowError('STATE_CORRUPT', 'Multiple attestation-verified Step Review events target the same reviewer attestation.', {
543
+ reviewerAttestationHash,
544
+ eventIds: linkedVerifiedEvents.map((event) => event.eventId),
545
+ });
546
+ }
547
+ }
548
+ }
549
+
550
+ function stripStepRuntime(step: TaskStep) {
551
+ const { status: _status, evidence: _evidence, failurePause: _failurePause, ...definition } = step;
552
+ return definition;
553
+ }
554
+
555
+ function requireTaskStep(task: TaskState, stepId: string): TaskStep {
556
+ const step = task.steps.find((candidate) => candidate.id === stepId);
557
+ if (!step) throw new WorkflowError('NOT_FOUND', `Step not found: ${stepId}`);
558
+ return step;
559
+ }
560
+
561
+ function eventMatchesBinding(event: StepReviewEvent, binding: StrictStepBinding): boolean {
562
+ return event.stepId === binding.stepId
563
+ && event.stepDefinitionHash === binding.stepDefinitionHash
564
+ && event.completionCommit === binding.completionCommit
565
+ && event.checksHash === binding.checksHash
566
+ && event.planHash === binding.planHash
567
+ && event.evidenceHash === binding.evidenceHash;
568
+ }
569
+
570
+ function stepReviewEventsMatchStructuralBinding(left: StepReviewEvent, right: StepReviewEvent): boolean {
571
+ return left.taskId === right.taskId
572
+ && left.stepId === right.stepId
573
+ && left.stepDefinitionHash === right.stepDefinitionHash
574
+ && left.completionCommit === right.completionCommit
575
+ && left.checksHash === right.checksHash
576
+ && left.planHash === right.planHash
577
+ && left.evidenceHash === right.evidenceHash;
578
+ }
579
+
580
+ function validateStepReviewRecordInput(current: CurrentStrictStepReviewCycle, input: StepReviewRecordInput): void {
581
+ const reviewer = typeof input.review.reviewer === 'string' ? input.review.reviewer.trim() : '';
582
+ const summary = typeof input.review.summary === 'string' ? input.review.summary.trim() : '';
583
+ if (!reviewer || !summary) {
584
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review requires reviewer identity and summary.', {
585
+ stepId: current.binding.stepId,
586
+ });
587
+ }
588
+ if (!Array.isArray(input.review.findings)) {
589
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings must be an array.', {
590
+ stepId: current.binding.stepId,
591
+ });
592
+ }
593
+ if (input.review.status === 'pending' || !REVIEW_DECISIONS.has(input.review.status)) {
594
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review status is invalid.', {
595
+ stepId: current.binding.stepId,
596
+ status: input.review.status,
597
+ });
598
+ }
599
+ for (const finding of input.review.findings) {
600
+ if (!finding || typeof finding !== 'object') {
601
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings must be objects.', {
602
+ stepId: current.binding.stepId,
603
+ });
604
+ }
605
+ if (typeof finding.id !== 'string' || !finding.id.trim()) {
606
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings require an id.', {
607
+ stepId: current.binding.stepId,
608
+ });
609
+ }
610
+ if (!REVIEW_FINDING_SEVERITIES.has(finding.severity)) {
611
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review findings severity is invalid.', {
612
+ stepId: current.binding.stepId,
613
+ findingId: finding.id,
614
+ severity: finding.severity,
615
+ });
616
+ }
617
+ if (finding.requirement !== null && (typeof finding.requirement !== 'string' || !finding.requirement.trim())) {
618
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review finding requirement must be null or a non-empty string.', {
619
+ stepId: current.binding.stepId,
620
+ findingId: finding.id,
621
+ });
622
+ }
623
+ for (const [label, value] of [
624
+ ['summary', finding.summary],
625
+ ['evidence', finding.evidence],
626
+ ['requiredAction', finding.requiredAction],
627
+ ] as const) {
628
+ if (typeof value !== 'string' || !value.trim()) {
629
+ throw new WorkflowError('INVALID_ARGUMENT', `Strict Step Review finding ${label} is required.`, {
630
+ stepId: current.binding.stepId,
631
+ findingId: finding.id,
632
+ });
633
+ }
634
+ }
635
+ validateFindingRemediationRoute(finding, current.binding.stepId);
636
+ }
637
+ if (input.review.status === 'passed' && input.review.findings.length > 0) {
638
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review cannot mark passed while findings remain.', {
639
+ stepId: current.binding.stepId,
640
+ });
641
+ }
642
+ if (input.review.status === 'failed' && input.review.findings.length === 0) {
643
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review failure must include at least one finding.', {
644
+ stepId: current.binding.stepId,
645
+ });
646
+ }
647
+ assertHash64(input.reviewerOutputHash, 'Strict Step Review reviewer output hash');
648
+ if (!GIT_SHA_PATTERN.test(input.reviewedCommit)) {
649
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review reviewed commit is not a full Git SHA.', {
650
+ stepId: current.binding.stepId,
651
+ reviewedCommit: input.reviewedCommit,
652
+ });
653
+ }
654
+ if (input.reviewedCommit !== current.binding.completionCommit) {
655
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review must bind the exact completion commit.', {
656
+ stepId: current.binding.stepId,
657
+ reviewedCommit: input.reviewedCommit,
658
+ completionCommit: current.binding.completionCommit,
659
+ });
660
+ }
661
+ if (input.isolationResult !== 'verified' && input.isolationResult !== 'unverified') {
662
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review isolation result is invalid.', {
663
+ stepId: current.binding.stepId,
664
+ isolationResult: input.isolationResult,
665
+ });
666
+ }
667
+ }
668
+
669
+ function reuseReviewRecordedEvent(
670
+ current: CurrentStrictStepReviewCycle,
671
+ input: StepReviewRecordInput,
672
+ ): StepReviewEvent {
673
+ const existing = current.reviewRecordedEvent;
674
+ if (!existing) throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review review-recorded event is unexpectedly missing.', {
675
+ stepId: current.binding.stepId,
676
+ });
677
+ if (existing.decision !== input.review.status) {
678
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review already recorded a conflicting decision for this completion commit.', {
679
+ stepId: current.binding.stepId,
680
+ existingDecision: existing.decision,
681
+ requestedDecision: input.review.status,
682
+ completionCommit: current.binding.completionCommit,
683
+ });
684
+ }
685
+ const requestedRouting = reviewRemediationEventFields(input.review);
686
+ if (
687
+ (existing.remediationRoute ?? null) !== (requestedRouting.remediationRoute ?? null)
688
+ || canonicalJsonStringify(existing.planConflicts ?? []) !== canonicalJsonStringify(requestedRouting.planConflicts ?? [])
689
+ ) {
690
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review already recorded a conflicting remediation route for this completion commit.', {
691
+ stepId: current.binding.stepId,
692
+ completionCommit: current.binding.completionCommit,
693
+ existingRoute: existing.remediationRoute ?? null,
694
+ requestedRoute: requestedRouting.remediationRoute ?? null,
695
+ });
696
+ }
697
+ return existing;
698
+ }
699
+
700
+ function reuseReviewerAttestationEvent(
701
+ current: CurrentStrictStepReviewCycle,
702
+ reviewRecordedEvent: StepReviewEvent,
703
+ input: StepReviewRecordInput,
704
+ ): ReviewerAttestationEvent {
705
+ const existing = current.reviewerAttestationEvent;
706
+ if (!existing) throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation is unexpectedly missing.', {
707
+ stepId: current.binding.stepId,
708
+ });
709
+ validateAttestationBinding(current.binding, reviewRecordedEvent, existing);
710
+ const reviewer = input.review.reviewer.trim();
711
+ if (existing.reviewer !== reviewer
712
+ || existing.isolationResult !== input.isolationResult
713
+ || existing.outputHash !== input.reviewerOutputHash
714
+ || existing.reviewedCommit !== input.reviewedCommit) {
715
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review already recorded a conflicting reviewer attestation for this completion commit.', {
716
+ stepId: current.binding.stepId,
717
+ completionCommit: current.binding.completionCommit,
718
+ existingReviewer: existing.reviewer,
719
+ requestedReviewer: reviewer,
720
+ existingIsolationResult: existing.isolationResult,
721
+ requestedIsolationResult: input.isolationResult,
722
+ existingOutputHash: existing.outputHash,
723
+ requestedOutputHash: input.reviewerOutputHash,
724
+ existingReviewedCommit: existing.reviewedCommit,
725
+ requestedReviewedCommit: input.reviewedCommit,
726
+ });
727
+ }
728
+ return existing;
729
+ }
730
+
731
+ function reuseAttestationVerifiedEvent(
732
+ current: CurrentStrictStepReviewCycle,
733
+ reviewRecordedEvent: StepReviewEvent,
734
+ reviewerAttestationEvent: ReviewerAttestationEvent,
735
+ ): StepReviewEvent {
736
+ const existing = current.attestationVerifiedEvent;
737
+ if (!existing) throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification is unexpectedly missing.', {
738
+ stepId: current.binding.stepId,
739
+ });
740
+ validateAttestationVerifiedChain(current.binding, reviewRecordedEvent, reviewerAttestationEvent, existing);
741
+ return existing;
742
+ }
743
+
744
+ function validateAttestationBinding(
745
+ binding: StrictStepBinding,
746
+ reviewRecordedEvent: StepReviewEvent,
747
+ attestation: ReviewerAttestationEvent,
748
+ ): void {
749
+ if (attestation.stepId !== binding.stepId
750
+ || attestation.taskId !== reviewRecordedEvent.taskId
751
+ || attestation.reviewedCommit !== binding.completionCommit
752
+ || attestation.reviewEventHash !== reviewRecordedEvent.eventHash
753
+ || attestation.planHash !== binding.planHash
754
+ || attestation.evidenceHash !== binding.evidenceHash) {
755
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review reviewer attestation does not match the current completion binding.', {
756
+ stepId: binding.stepId,
757
+ completionCommit: binding.completionCommit,
758
+ reviewEventId: reviewRecordedEvent.eventId,
759
+ attestationEventId: attestation.eventId,
760
+ });
761
+ }
762
+ }
763
+
764
+ function validateHistoricalAttestationBinding(
765
+ reviewRecordedEvent: StepReviewEvent,
766
+ attestation: ReviewerAttestationEvent,
767
+ ): void {
768
+ if (reviewRecordedEvent.eventType !== 'review-recorded') {
769
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation must target a review-recorded Step Review event.', {
770
+ reviewEventId: reviewRecordedEvent.eventId,
771
+ reviewEventType: reviewRecordedEvent.eventType,
772
+ attestationEventId: attestation.eventId,
773
+ });
774
+ }
775
+ if (attestation.taskId !== reviewRecordedEvent.taskId
776
+ || attestation.stepId !== reviewRecordedEvent.stepId
777
+ || attestation.reviewedCommit !== reviewRecordedEvent.completionCommit
778
+ || attestation.reviewEventHash !== reviewRecordedEvent.eventHash
779
+ || attestation.planHash !== reviewRecordedEvent.planHash
780
+ || attestation.evidenceHash !== reviewRecordedEvent.evidenceHash) {
781
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation does not match the referenced review-recorded Step Review event binding.', {
782
+ reviewEventId: reviewRecordedEvent.eventId,
783
+ attestationEventId: attestation.eventId,
784
+ reviewEventHash: reviewRecordedEvent.eventHash,
785
+ });
786
+ }
787
+ }
788
+
789
+ function validateAttestationVerifiedChain(
790
+ binding: StrictStepBinding,
791
+ reviewRecordedEvent: StepReviewEvent,
792
+ reviewerAttestationEvent: ReviewerAttestationEvent,
793
+ verifiedEvent: StepReviewEvent,
794
+ ): void {
795
+ if (!eventMatchesBinding(verifiedEvent, binding)) {
796
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification does not match the current completion binding.', {
797
+ stepId: binding.stepId,
798
+ eventId: verifiedEvent.eventId,
799
+ });
800
+ }
801
+ if (reviewerAttestationEvent.isolationResult !== 'verified') {
802
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification cannot exist for an unverified isolation result.', {
803
+ stepId: binding.stepId,
804
+ attestationEventId: reviewerAttestationEvent.eventId,
805
+ verifiedEventId: verifiedEvent.eventId,
806
+ });
807
+ }
808
+ if (verifiedEvent.reviewerAttestationHash !== reviewerAttestationEvent.eventHash) {
809
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification does not reference the expected attestation event.', {
810
+ stepId: binding.stepId,
811
+ attestationEventId: reviewerAttestationEvent.eventId,
812
+ verifiedEventId: verifiedEvent.eventId,
813
+ });
814
+ }
815
+ if (verifiedEvent.decision !== reviewRecordedEvent.decision) {
816
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review attestation verification decision does not match the review event.', {
817
+ stepId: binding.stepId,
818
+ reviewEventId: reviewRecordedEvent.eventId,
819
+ verifiedEventId: verifiedEvent.eventId,
820
+ reviewDecision: reviewRecordedEvent.decision,
821
+ verifiedDecision: verifiedEvent.decision,
822
+ });
823
+ }
824
+ validateAttestationBinding(binding, reviewRecordedEvent, reviewerAttestationEvent);
825
+ }
826
+
827
+ function validateHistoricalAttestationVerifiedChain(
828
+ reviewRecordedEvent: StepReviewEvent,
829
+ reviewerAttestationEvent: ReviewerAttestationEvent,
830
+ verifiedEvent: StepReviewEvent,
831
+ ): void {
832
+ if (reviewRecordedEvent.eventType !== 'review-recorded') {
833
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event must link to a review-recorded Step Review event.', {
834
+ reviewEventId: reviewRecordedEvent.eventId,
835
+ reviewEventType: reviewRecordedEvent.eventType,
836
+ verifiedEventId: verifiedEvent.eventId,
837
+ });
838
+ }
839
+ if (!stepReviewEventsMatchStructuralBinding(verifiedEvent, reviewRecordedEvent)) {
840
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event does not match the referenced review-recorded binding.', {
841
+ reviewEventId: reviewRecordedEvent.eventId,
842
+ verifiedEventId: verifiedEvent.eventId,
843
+ });
844
+ }
845
+ if (reviewerAttestationEvent.isolationResult !== 'verified') {
846
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event cannot link to an unverified reviewer attestation.', {
847
+ reviewEventId: reviewRecordedEvent.eventId,
848
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
849
+ verifiedEventId: verifiedEvent.eventId,
850
+ });
851
+ }
852
+ if (verifiedEvent.reviewerAttestationHash !== reviewerAttestationEvent.eventHash) {
853
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event does not reference the expected reviewer attestation.', {
854
+ reviewEventId: reviewRecordedEvent.eventId,
855
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
856
+ verifiedEventId: verifiedEvent.eventId,
857
+ });
858
+ }
859
+ if (verifiedEvent.decision !== reviewRecordedEvent.decision) {
860
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review decision does not match the referenced review-recorded event.', {
861
+ reviewEventId: reviewRecordedEvent.eventId,
862
+ reviewerAttestationEventId: reviewerAttestationEvent.eventId,
863
+ verifiedEventId: verifiedEvent.eventId,
864
+ reviewDecision: reviewRecordedEvent.decision,
865
+ verifiedDecision: verifiedEvent.decision,
866
+ });
867
+ }
868
+ validateHistoricalAttestationBinding(reviewRecordedEvent, reviewerAttestationEvent);
869
+ }
870
+
871
+ function appendStepReviewEvent(
872
+ store: FileStateStore,
873
+ task: TaskState,
874
+ event: Omit<StepReviewEvent, 'previousEventHash' | 'eventHash'>,
875
+ ): StepReviewEvent {
876
+ return store.appendSidecarEvent<StepReviewEvent>(
877
+ store.taskRoot(task.projectId, task.id),
878
+ STEP_REVIEW_EVENTS_SIDECAR,
879
+ event,
880
+ {
881
+ validateEvent: (candidate, context) => validateStoredStepReviewEvent(candidate, task.id, context.file, context.line),
882
+ },
883
+ );
884
+ }
885
+
886
+ function appendReviewerAttestationEvent(
887
+ store: FileStateStore,
888
+ task: TaskState,
889
+ event: Omit<ReviewerAttestationEvent, 'previousEventHash' | 'eventHash'>,
890
+ ): ReviewerAttestationEvent {
891
+ return store.appendSidecarEvent<ReviewerAttestationEvent>(
892
+ store.taskRoot(task.projectId, task.id),
893
+ REVIEWER_ATTESTATIONS_SIDECAR,
894
+ event,
895
+ {
896
+ validateEvent: (candidate, context) =>
897
+ validateStoredReviewerAttestationEvent(candidate, task.id, context.file, context.line),
898
+ },
899
+ );
900
+ }
901
+
902
+ function validateStoredStepReviewEvent(
903
+ event: StepReviewEvent,
904
+ expectedTaskId: string,
905
+ file: string,
906
+ line: number,
907
+ ): void {
908
+ if (event.taskId !== expectedTaskId) {
909
+ throw new WorkflowError('STATE_CORRUPT', 'Step Review event task binding does not match its task root.', {
910
+ file,
911
+ line,
912
+ eventId: event.eventId,
913
+ expectedTaskId,
914
+ actualTaskId: event.taskId,
915
+ });
916
+ }
917
+ assertPositiveInteger(event.taskRevision, 'Step Review taskRevision', file, line, event.eventId);
918
+ assertNonEmptyString(event.stepId, 'Step Review stepId', file, line, event.eventId);
919
+ assertHash64(event.stepDefinitionHash, 'Step Review stepDefinitionHash');
920
+ assertGitSha(event.completionCommit, 'Step Review completionCommit', file, line, event.eventId);
921
+ assertHash64(event.checksHash, 'Step Review checksHash');
922
+ assertHash64(event.planHash, 'Step Review planHash');
923
+ assertOptionalHash64(event.evidenceHash, 'Step Review evidenceHash', file, line, event.eventId);
924
+ if (!STEP_REVIEW_EVENT_TYPES.has(event.eventType)) {
925
+ throw new WorkflowError('STATE_CORRUPT', 'Step Review eventType is invalid.', {
926
+ file,
927
+ line,
928
+ eventId: event.eventId,
929
+ eventType: event.eventType,
930
+ });
931
+ }
932
+ if (!REVIEW_DECISIONS.has(event.decision)) {
933
+ throw new WorkflowError('STATE_CORRUPT', 'Step Review decision is invalid.', {
934
+ file,
935
+ line,
936
+ eventId: event.eventId,
937
+ decision: event.decision,
938
+ });
939
+ }
940
+ if (event.eventType === 'pending') {
941
+ if (event.decision !== 'pending' || event.reviewerAttestationHash !== null
942
+ || event.remediationRoute !== undefined || event.planConflicts !== undefined
943
+ || event.remediationCauses !== undefined) {
944
+ throw new WorkflowError('STATE_CORRUPT', 'Pending Step Review event must remain unattested with decision pending.', {
945
+ file,
946
+ line,
947
+ eventId: event.eventId,
948
+ });
949
+ }
950
+ }
951
+ if (event.eventType === 'review-recorded') {
952
+ if (event.decision === 'pending' || event.reviewerAttestationHash !== null) {
953
+ throw new WorkflowError('STATE_CORRUPT', 'Review-recorded Step Review event must carry a terminal review decision and no attestation hash.', {
954
+ file,
955
+ line,
956
+ eventId: event.eventId,
957
+ });
958
+ }
959
+ validateStoredReviewRemediation(event, file, line);
960
+ }
961
+ if (event.eventType === 'attestation-verified') {
962
+ if (event.decision === 'pending' || typeof event.reviewerAttestationHash !== 'string'
963
+ || event.remediationRoute !== undefined || event.planConflicts !== undefined
964
+ || event.remediationCauses !== undefined) {
965
+ throw new WorkflowError('STATE_CORRUPT', 'Attestation-verified Step Review event must carry a terminal decision and attestation hash.', {
966
+ file,
967
+ line,
968
+ eventId: event.eventId,
969
+ });
970
+ }
971
+ assertHash64(event.reviewerAttestationHash, 'Step Review reviewerAttestationHash');
972
+ }
973
+ }
974
+
975
+ function reviewRemediationEventFields(
976
+ review: ReviewInput,
977
+ ): Pick<StepReviewEvent, 'remediationRoute' | 'planConflicts' | 'remediationCauses'> | Record<string, never> {
978
+ if (review.status !== 'failed') return {};
979
+ const replanFindings = review.findings.filter((finding) => (finding.route ?? 'fix') === 'replan');
980
+ const remediationCauses = review.findings.flatMap((finding) => finding.causeId ? [{
981
+ findingId: finding.id,
982
+ causeId: finding.causeId,
983
+ causeFingerprint: reviewerCauseFingerprint(finding.causeId),
984
+ route: finding.route ?? 'fix',
985
+ planConflict: (finding.route ?? 'fix') === 'replan' && finding.planConflict !== null,
986
+ }] : []);
987
+ return {
988
+ remediationRoute: replanFindings.length > 0 ? 'replan' : 'fix',
989
+ planConflicts: replanFindings.map((finding) => ({
990
+ findingId: finding.id,
991
+ planClause: finding.planConflict!.planClause,
992
+ whyFixCannotFit: finding.planConflict!.whyFixCannotFit,
993
+ minimalPlanChange: finding.planConflict!.minimalPlanChange,
994
+ })),
995
+ ...(remediationCauses.length > 0 ? { remediationCauses } : {}),
996
+ };
997
+ }
998
+
999
+ function validateFindingRemediationRoute(
1000
+ finding: ReviewInput['findings'][number],
1001
+ stepId: string,
1002
+ ): void {
1003
+ if (finding.causeId !== undefined
1004
+ && !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(finding.causeId)) {
1005
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review finding causeId is invalid.', {
1006
+ stepId,
1007
+ findingId: finding.id,
1008
+ });
1009
+ }
1010
+ const route = finding.route ?? 'fix';
1011
+ if (route !== 'fix' && route !== 'replan') {
1012
+ throw new WorkflowError('INVALID_ARGUMENT', 'Strict Step Review finding remediation route is invalid.', {
1013
+ stepId,
1014
+ findingId: finding.id,
1015
+ route,
1016
+ });
1017
+ }
1018
+ if (route === 'fix') {
1019
+ if (finding.planConflict !== undefined && finding.planConflict !== null) {
1020
+ throw new WorkflowError('INVALID_ARGUMENT', 'A fix finding cannot carry a Plan conflict.', {
1021
+ stepId,
1022
+ findingId: finding.id,
1023
+ });
1024
+ }
1025
+ return;
1026
+ }
1027
+ const conflict = finding.planConflict;
1028
+ if (!conflict
1029
+ || !conflict.planClause?.trim()
1030
+ || !conflict.whyFixCannotFit?.trim()
1031
+ || !conflict.minimalPlanChange?.trim()) {
1032
+ throw new WorkflowError('INVALID_ARGUMENT', 'A replan finding requires an exact Plan clause, obstruction, and minimal Plan change.', {
1033
+ stepId,
1034
+ findingId: finding.id,
1035
+ });
1036
+ }
1037
+ }
1038
+
1039
+ function validateStoredReviewRemediation(event: StepReviewEvent, file: string, line: number): void {
1040
+ if (event.decision !== 'failed') {
1041
+ if (event.remediationRoute !== undefined || event.planConflicts !== undefined
1042
+ || event.remediationCauses !== undefined) {
1043
+ throw new WorkflowError('STATE_CORRUPT', 'Only a failed review-recorded event may carry remediation routing.', {
1044
+ file,
1045
+ line,
1046
+ eventId: event.eventId,
1047
+ });
1048
+ }
1049
+ return;
1050
+ }
1051
+ const route = event.remediationRoute ?? 'fix';
1052
+ const conflicts = event.planConflicts ?? [];
1053
+ const causes = event.remediationCauses ?? [];
1054
+ if (route !== 'fix' && route !== 'replan') {
1055
+ throw new WorkflowError('STATE_CORRUPT', 'Review-recorded remediation route is invalid.', { file, line, eventId: event.eventId });
1056
+ }
1057
+ if ((route === 'fix' && conflicts.length > 0) || (route === 'replan' && conflicts.length === 0)) {
1058
+ throw new WorkflowError('STATE_CORRUPT', 'Review-recorded remediation route conflicts with its Plan evidence.', {
1059
+ file,
1060
+ line,
1061
+ eventId: event.eventId,
1062
+ route,
1063
+ });
1064
+ }
1065
+ for (const conflict of conflicts) {
1066
+ if (!conflict.findingId?.trim()
1067
+ || !conflict.planClause?.trim()
1068
+ || !conflict.whyFixCannotFit?.trim()
1069
+ || !conflict.minimalPlanChange?.trim()) {
1070
+ throw new WorkflowError('STATE_CORRUPT', 'Review-recorded Plan conflict evidence is incomplete.', {
1071
+ file,
1072
+ line,
1073
+ eventId: event.eventId,
1074
+ });
1075
+ }
1076
+ }
1077
+ for (const cause of causes) {
1078
+ if (!cause.findingId?.trim()
1079
+ || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(cause.causeId)
1080
+ || cause.causeFingerprint !== reviewerCauseFingerprint(cause.causeId)
1081
+ || cause.planConflict !== (cause.route === 'replan')) {
1082
+ throw new WorkflowError('STATE_CORRUPT', 'Review-recorded cause evidence is invalid.', {
1083
+ file, line, eventId: event.eventId,
1084
+ });
1085
+ }
1086
+ }
1087
+ }
1088
+
1089
+ function validateStoredReviewerAttestationEvent(
1090
+ event: ReviewerAttestationEvent,
1091
+ expectedTaskId: string,
1092
+ file: string,
1093
+ line: number,
1094
+ ): void {
1095
+ if (event.taskId !== expectedTaskId) {
1096
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation task binding does not match its task root.', {
1097
+ file,
1098
+ line,
1099
+ eventId: event.eventId,
1100
+ expectedTaskId,
1101
+ actualTaskId: event.taskId,
1102
+ });
1103
+ }
1104
+ assertNonEmptyString(event.stepId, 'Reviewer attestation stepId', file, line, event.eventId);
1105
+ assertNonEmptyString(event.reviewer, 'Reviewer attestation reviewer', file, line, event.eventId);
1106
+ if (event.isolationResult !== 'verified' && event.isolationResult !== 'unverified') {
1107
+ throw new WorkflowError('STATE_CORRUPT', 'Reviewer attestation isolation result is invalid.', {
1108
+ file,
1109
+ line,
1110
+ eventId: event.eventId,
1111
+ isolationResult: event.isolationResult,
1112
+ });
1113
+ }
1114
+ assertHash64(event.outputHash, 'Reviewer attestation outputHash');
1115
+ assertGitSha(event.reviewedCommit, 'Reviewer attestation reviewedCommit', file, line, event.eventId);
1116
+ assertHash64(event.reviewEventHash, 'Reviewer attestation reviewEventHash');
1117
+ assertHash64(event.planHash, 'Reviewer attestation planHash');
1118
+ assertOptionalHash64(event.evidenceHash, 'Reviewer attestation evidenceHash', file, line, event.eventId);
1119
+ }
1120
+
1121
+ function findLastIndex<T>(items: readonly T[], predicate: (value: T) => boolean): number {
1122
+ for (let index = items.length - 1; index >= 0; index -= 1) {
1123
+ if (predicate(items[index]!)) return index;
1124
+ }
1125
+ return -1;
1126
+ }
1127
+
1128
+ function indexStepReviewEventsByHash(
1129
+ stepReviewEvents: readonly StepReviewEvent[],
1130
+ eventType: StepReviewEventType,
1131
+ ): Map<string, StepReviewEvent[]> {
1132
+ const indexed = new Map<string, StepReviewEvent[]>();
1133
+ for (const event of stepReviewEvents) {
1134
+ if (event.eventType !== eventType) continue;
1135
+ const matches = indexed.get(event.eventHash) ?? [];
1136
+ matches.push(event);
1137
+ indexed.set(event.eventHash, matches);
1138
+ }
1139
+ return indexed;
1140
+ }
1141
+
1142
+ function indexReviewerAttestationsByHash(
1143
+ reviewerAttestations: readonly ReviewerAttestationEvent[],
1144
+ ): Map<string, ReviewerAttestationEvent[]> {
1145
+ const indexed = new Map<string, ReviewerAttestationEvent[]>();
1146
+ for (const event of reviewerAttestations) {
1147
+ const matches = indexed.get(event.eventHash) ?? [];
1148
+ matches.push(event);
1149
+ indexed.set(event.eventHash, matches);
1150
+ }
1151
+ return indexed;
1152
+ }
1153
+
1154
+ function assertPositiveInteger(value: unknown, label: string, file: string, line: number, eventId: string): void {
1155
+ if (!Number.isInteger(value) || Number(value) < 1) {
1156
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a positive integer.`, {
1157
+ file,
1158
+ line,
1159
+ eventId,
1160
+ value,
1161
+ });
1162
+ }
1163
+ }
1164
+
1165
+ function assertNonEmptyString(value: unknown, label: string, file: string, line: number, eventId: string): void {
1166
+ if (typeof value !== 'string' || !value.trim()) {
1167
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a non-empty string.`, {
1168
+ file,
1169
+ line,
1170
+ eventId,
1171
+ value,
1172
+ });
1173
+ }
1174
+ }
1175
+
1176
+ function assertGitSha(value: unknown, label: string, file: string, line: number, eventId: string): void {
1177
+ if (typeof value !== 'string' || !GIT_SHA_PATTERN.test(value)) {
1178
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be a full Git SHA.`, {
1179
+ file,
1180
+ line,
1181
+ eventId,
1182
+ value,
1183
+ });
1184
+ }
1185
+ }
1186
+
1187
+ function assertOptionalHash64(value: unknown, label: string, file: string, line: number, eventId: string): void {
1188
+ if (value === null) return;
1189
+ if (typeof value !== 'string') {
1190
+ throw new WorkflowError('STATE_CORRUPT', `${label} must be null or a sha256 hex digest.`, {
1191
+ file,
1192
+ line,
1193
+ eventId,
1194
+ value,
1195
+ });
1196
+ }
1197
+ assertHash64(value, label);
1198
+ }