diff --git a/README.md b/README.md index 7644c67..6d54d94 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,13 @@ defaults are applied; output aliases such as `EvidenceRef` describe parsed data. traces, or other evidence. - `hasna.work_run.v1`: normalized run receipt for agent, command, workflow, loop, eval, test, deploy, or review work. +- `hasna.task_to_pr_projection.v1`: immutable, provider-neutral identity + projection for one task-to-PR attempt. It binds the Todos root/PR-group/leaf + and replay cursor, the Codewith run/profile route, the Repos + repo/worktree/branch/writer lease, exact-head review and merge-CAS refs, + repair/recovery/cancellation preservation, cleanup, rollback, and optional + OpenLoops invocation context without becoming a receipt, queue, store, or + canonical history. - `hasna.decision_envelope.v1`: decision record with selected/skipped resources, rationale, actor, costs, obligations, redactions, and evidence. - `hasna.cost_estimate.v1`: money and token estimates with provider, model, @@ -382,13 +389,187 @@ Common resource-kind mappings: | `open-economy` | `cost`, `budget`; budget choices are emitted as `DecisionEnvelope` contracts, not resource kinds | | `open-monitor` | `alert`, `incident`, `machine`, `report` | +## Task-to-PR projection + +`TaskToPrProjection` is a strict reference projection, not a `WorkReceipt`, +`PRReceipt`, queue, database, event store, or replacement for any owning +package. `WorkRun` remains the execution receipt. `EvidenceRef` and +`ProofBundle` remain evidence and verification records; `DecisionEnvelope` +remains a decision record; `CapabilityCard` remains an admission snapshot; +`ResourcePointer` remains a generic resource link; and `AgentTrajectory` +remains a bounded trace. The projection only binds their owner-resolved refs. + +Authority is fixed: + +- Todos owns the canonical root request, PR group, leaf task, attempt, + writer generation, terminal disposition, repair state, and append-only + lifecycle history. +- Codewith owns durable run admission, worker actor and assignment, opaque + profile/route selection, and runtime state. +- Repos owns repository, worktree, branch, writer lease/fence refs, and cleanup. +- Infinity may execute an admitted attempt but is only an adapter; it does not + become an authority or history store. +- TAI may render the projection read-only; it does not write lifecycle state. +- OpenLoops may contribute one optional invocation ref as orchestration + context. It owns no Codewith goal, Todos task or history, Repos lease, + review, merge guard, or merge outcome. + +Every `TaskToPrRef` has one role, one allowed authority, one lowercase SHA-256 +owner-record digest, one explicit redaction state, and one structural +nonsemantic id: +`role:authority:opaque-`. This makes owner +resolution schema-driven rather than dependent on a semantic-keyword blacklist. +Canonical objects are dereferenced from their owning packages instead of +embedded as mutable payloads. The projection id +(`task_to_pr_projection:opaque-*`) and attempt nonce +(`attempt_nonce:opaque-*`) each require an independent 128-bit lowercase +hexadecimal surrogate. +`TaskToPrEvidenceRef` is deliberately smaller than `EvidencePointer`: it only +allows `evidence:opaque-`, the immutable +digest, and `partial`/`full` redaction, so a +projection cannot inline a URI, summary, command output, or secret-adjacent +payload. + +| Projection field | Cardinality | Authority and invariant | +| --- | --- | --- | +| `workRunRef` | exactly 1 | Codewith; points to the existing `WorkRun` receipt | +| `rootRequestRef` / `prGroupRef` / `leafTaskRef` | exactly 1 each | Todos; immutable canonical identities | +| `attempt.ref` / `attempt.nonce` | exactly 1 each | Todos attempt identity; every retry uses a fresh nonce | +| admission / worker actor / worker assignment / runtime refs | exactly 1 each | distinct redacted Codewith owner records; admission carries an exact writer-generation binding, and every successor attempt rotates admission and assignment in addition to the attempt-scoped runtime identity | +| writer generation / lease / fence refs | exactly 1 each | generation is Todos-bound; lease/fence are redacted Repos refs, never a fence value | +| provider profile / route refs | exactly 1 each | redacted opaque Codewith refs; never provider account or credential data | +| repo / worktree / branch plus base and branch heads | exactly 1 binding | canonical Repos refs with exact Git object ids | +| event stream / replay cursor / sequence / prefix digest | exactly 1 cursor | Todos append-only history is referenced, never copied; sequences are nonnegative safe integers | +| `handoff` | 0 or 1 current transition | Todos ref binding prior/next attempts, prior/next generations, and the prior WorkRun plus distinct stop and revocation evidence identities/digests; the full record is immutable under one canonical ref, while a later handoff requires both a fresh canonical role/authority/id and a fresh digest; mutually exclusive with recovery | +| `pullRequestRef` | 0 or 1 | Todos PR-group identity; required by review or merge state | +| exact-head proof | required before review/merge | exact expected base = provider PR base and local head = remote head = provider PR head, with equality and CI proof refs; same-base/head facts are immutable and every base/head change requires fresh proof identities and digests | +| `reviews` | 0 or more unique exact-base/exact-head reviews | review ref, reviewer/run, proof bundle, PR, immutable base, and immutable head; same-base/head history preserves the prior array as an exact immutable prefix and can only append, while a changed base or head requires fresh review, review-run, and proof-bundle identities and digests | +| `repair` | exactly 1 state | cumulative Todos cycle `0..2`; never decrements or resets; entering `repairing` consumes exactly one new cycle, exhausted work cannot re-enter repair, and repair state freezes after terminal disposition | +| merge guard / outcome | 0 or 1 each | guard binds reviewed base/head and proof; only `merge_ready` may carry `eligible`, terminal outcomes require a `consumed` guard, and failed/blocked/cancelled history may only retain a revoked guard; outcome classifies base drift independently from head drift and binds the exact immutable guard | +| recovery / cancellation | recovery is mutually exclusive with handoff and cancellation; 0 or 1 each | preservation refs are an exact one-per-role set for root/group/leaf/repo/worktree/branch/event and present PR; recovery binds canonically distinct prior attempt/generation/WorkRun refs, distinct stop/revocation evidence, and a fresh successor nonce/generation | +| `terminalDispositionRef` | exactly 1 in terminal states; absent otherwise | durable Todos owner fact for the terminal disposition; immutable after establishment | +| cleanup eligibility / outcome | 0 or 1 each | Repos-owned; eligibility binds the terminal disposition, current replay cursor, exact writer lease, canonical worktree, distinct lease-revocation and consumed-event evidence, and must be `eligible` before deletion; outcome binds the exact eligibility decision | +| rollback plan / outcome | 0 or 1 each | plan targets a commit or branch, remains immutable under one canonical ref, and uses a fresh canonical id and digest when the plan or target changes; immutable outcome binds the exact plan and target | +| `provenanceLedger` | exactly 1 append-only identity ledger | immutable exact-prefix tombstones for the stable projection id, WorkRun, attempt, admission, worker actor/assignment, nonce, runtime, writer generation/lease/fence, provider profile/route, replay cursor/prefix, repair state/latest repair, handoff, recovery, merge guard, cleanup eligibility, rollback plan, terminal disposition, and base/head-bound equality/CI/review/provider-receipt identities; active identities must be represented exactly and all ref ids/digests share one global uniqueness domain | +| OpenLoops invocation | 0 or 1 | optional redacted context only | +| adapter extensions | 0 or more unique mode/schema pairs | `local` or `cloud` ref plus digest only; every extension schema must use the permanently reserved `hasna.task_to_pr_adapter_extension.*` namespace | + +The top-level document deliberately has `createdAt` but no `updatedAt`. +Producers emit immutable snapshots under one stable top-level projection id; +the Todos stream/cursor remains the only canonical lifecycle history. Any +semantic change requires replay-sequence advancement with a fresh cursor and +prefix digest. `provenanceLedger` is not another event store: it is an +identity-only exact-prefix index over that history. Head-bound entries carry +both base and head, while admission, worker assignment, terminal disposition, +attempt nonce, and replay-prefix facts remain independently replay-safe. Ref +equality is exact across role, authority, id, digest, and redaction. + +`validateTaskToPrProjectionTransition` parses both snapshots before comparing +them. It rejects top-level id changes, identity-scope mutation, replay +regression, sequence advances without a fresh cursor/prefix, semantic drift +without an event advance, provenance truncation/reordering/mutation, inactive +identity reactivation, illegal lifecycle edges, partial attempt lineage +rotation, and successor attempts that reuse admission, worker assignment, +runtime, lease, fence, profile, or route identities. Handoff/recovery must bind +the immediately prior attempt, generation, and WorkRun; recovery and +cancellation preservation lists must contain exactly the required roles with +no duplicates or extras. Repair entry advances exactly one cycle, cannot occur +after exhaustion, and repair is immutable after terminal disposition. +Same-base/head exact-head and review facts remain immutable prefixes; a base or +head change rotates equality, CI, review, review-run, review-proof, and provider +receipt identities. Terminal disposition plus complete merge, cancellation, +cleanup, and rollback owner facts are immutable once present. +`admitted`, `running`, and `handed_off` snapshots cannot carry direct review +bindings or hidden merge-guard review refs, including on a denied guard. A +`reviewing` to `recovering` transition may retain the exact same-head +`exactHead` and review prefix, but recovery cannot invent them from a pre-review +snapshot or carry merge-guard review refs. Same-head reviews can only be appended after the exact prior array +prefix; reordering or prepending invalidates the transition. This schema has no review +supersession field, so producers must advance the reviewed head before emitting +a replacement review set. `merge_ready` is the only state that can carry an +`eligible` guard. Eligible and consumed guards require +`attempt.admissionWriterGenerationRef` to exactly equal the current +`attempt.writerGenerationRef`, so a stale admission cannot survive writer +generation rollover into merge readiness or a terminal merge outcome. A +terminal merge outcome requires the same guard authority +facts in `consumed` state; because guard owner records are immutable, consumption or +revocation uses a fresh guard ref/digest while preserving the eligible guard's +exact PR, base/head, review/proof, operator, provider-receipt, and mechanism +facts. Failed, blocked, and cancelled projections cannot retain eligibility. +Failed and blocked are durable terminal dispositions and do not advertise a +transition back to `recovering`; `merge_ready` must revoke into a supported +non-recovery state before any later recovery attempt. Direct +nonterminal-to-`cleanup_complete` edges are illegal, and a +destructive cleanup decision must prove terminal disposition, writer-lease +revocation, and consumption of the terminal event. +Consumers must reject unknown fields rather than retain unvalidated embedded +payloads. + +The exhaustive merge-authority matrix is: + +| Projection state | Allowed merge authority | +| --- | --- | +| admitted, running, handed_off, reviewing, repairing, recovering | absent, denied-without-outcome, or revoked-without-outcome | +| merge_ready | eligible without outcome | +| merged | consumed with `merged` outcome | +| closed_unmerged | consumed with `closed_unmerged`, `refused`, `head_drift`, or `base_drift` outcome | +| failed, blocked, cancelled | absent or revoked-without-outcome | +| cleanup_complete | absent, revoked-without-outcome, or a previously consumed terminal outcome | +| rolled_back | consumed with `merged` outcome | + +`canonicalizationVersion: 2` derives `identityDigest` from the ordered, +domain-separated tuple of role, authority, id, and digest for root request, PR +group, leaf task, repository, worktree, and branch, followed by the exact base +Git object id and frozen-scope digest. `canonicalizationVersion: 1` remains an +explicit legacy compatibility path using the prior root/group/leaf/repository +id/digest tuple and never silently upgrades to v2. The PR-group owner digest is +an input, never the derived output, so the binding has no circular dependency. +Digests on refs mean +the SHA-256 of the owning package's canonical, redacted record identity bytes; +they must never hash a credential, raw fence value, mutable payload, or +provider account record. + +Exact base/head binding is transitive: `remoteBranchRef` equals the canonical +repository branch ref; `expectedBase`, provider pull-request base, repository +base, every review base, guard base, outcome expected base, and all +base/head-bound provenance entries agree. The local, remote, and provider +pull-request heads are equal and backed by equality plus CI proof refs. +Equality, every CI proof, and every review proof obligation are globally unique +by canonical ref id and digest, so one proof cannot discharge two obligations. +An eligible merge guard references the exact canonical set of approved review +refs with no extras or omissions, every review proof, every CI proof, the +equality proof, an opaque provider receipt, and its CAS/expected-head mechanism. +A merged outcome observes the same base and head. Only `head_drift` may carry a +mismatched observed head and only `base_drift` may carry a mismatched observed +base; each drift outcome keeps the other dimension equal. Exact-head +verification, reviews, guard evaluation, and merge outcome are chronologically +ordered. + +Sensitive refs (`writer_lease`, `writer_fence`, `provider_profile`, +`provider_route`, `admission`, worker/runtime refs, `worktree`, +merge-provider refs, and adapter extensions) +require `partial` or `full` redaction. Strict objects reject fields such as +provider account ids, credentials, raw fence tokens, or embedded adapter +payloads. Local and cloud adapters serialize the same shared projection and +place provider-specific detail behind separately validated, redacted extension +refs. Extension schemas use the permanently reserved +`hasna.task_to_pr_adapter_extension.*` namespace, so adding unrelated canonical +schemas to `SCHEMA_IDS` cannot make previously accepted adapter data ambiguous. +`validateTaskToPrAdapterCoreEquivalence` parses both documents, requires +one or more local-only extensions in its first argument and one or more +cloud-only extensions in its second, removes only those validated +`adapterExtensions`, and requires byte-equivalent local and cloud core +projection data, including the complete cumulative provenance ledger. + ## Package Boundaries `@hasna/contracts` owns schemas, types, validators, examples, and conformance helpers. Owning packages still own storage and behavior. - `open-todos` owns tasks, task plans, locks, comments, and task evidence. -- `open-loops` owns loop and workflow execution. +- `open-loops` owns its own loop and workflow execution. In task-to-PR flows, + its invocation is optional reference-only orchestration context and confers + no authority over Codewith, Todos, Repos, review, or merge state. - `open-events` owns event envelopes, channels, delivery, replay, and notification semantics. - `open-actions` owns executable action manifests. @@ -435,11 +616,12 @@ native domain objects immediately. `ProofBundle`; task execution receipts as `WorkRun`; review gates as `ValidationPlan`; and workflow/run manifest pointers as compact task fields, not embedded handoff artifacts. -- `open-loops`: emit loop/workflow runs as `WorkRun`, audit traces as +- `open-loops`: emit its own loop/workflow runs as `WorkRun`, audit traces as `AgentTrajectory`, logs/artifacts as `EvidenceRef`, and verifier output as - `ProofBundle`. OpenLoops owns `WorkflowInvocation`, admission/work-item - queues, leases, workflow runs, retries, cancellation, worktrees, and run - artifacts. + `ProofBundle`. A task-to-PR adapter may add one redacted + `openLoopsInvocationRef`, but must not own or mirror the task-to-PR queue, + Todos history, Codewith admission/runtime state, Repos leases/worktrees, + review, or merge state. - `open-events`: emit and replay validated event envelopes to channels. OpenEvents delivers notifications only; it does not create workflow invocations, own queue state, or retry agent work. @@ -467,8 +649,8 @@ native domain objects immediately. template copying, setup wizards, source paths, and private metadata inside the scaffold package. - `open-automations`: keep deterministic app/product automation recipes and - connector/action recipes. Any agentic task, PR, review, or evaluation flow - must hand off to OpenLoops rather than creating a second workflow queue. + connector/action recipes. Agentic task, PR, and review flows hand off to the + canonical Todos/Codewith/Repos owners rather than creating a second queue. - `open-reports`: consume `ProofBundle`, `WorkRun`, `ContextPack`, `CostEstimate`, and `EvidenceRef` to render compact Markdown/JSON/HTML proof reports. @@ -479,10 +661,12 @@ native domain objects immediately. ## WorkflowInvocation And App Storage Boundary -The canonical agent-work root is a `WorkflowInvocation`, not a todo task. -Only actionable unfinished work needs a todo. OpenTodos remains the -human-visible intent ledger; OpenLoops owns the durable workflow root, -admission queue, execution lifecycle, and canonical run artifacts. +For task-to-PR work, the canonical root is the Todos root request and PR-group +history referenced by `TaskToPrProjection`; it is not a `WorkflowInvocation`. +Codewith owns durable admission/profile/runtime state and Repos owns +repo/worktree/writer-lease/cleanup state. A `WorkflowInvocation` can still be +useful inside OpenLoops' own workflow domain, but a task-to-PR projection may +only carry it as optional orchestration context. A workflow invocation should carry these fields at the boundary: @@ -499,11 +683,10 @@ A workflow invocation should carry these fields at the boundary: concurrency group - `outputPolicy`: when to write reports and when to create a follow-up task -OpenLoops admission/work items are first-class records with route key, -idempotency key, source/subject refs, project key/group, priority, status, -attempts, next-attempt time, lease expiry, loop/workflow/run ids, and last -reason. Status values should be explicit: `queued`, `deferred`, `admitted`, -`running`, `succeeded`, `failed`, `dead_letter`, or `cancelled`. +OpenLoops admission/work items remain first-class records only for OpenLoops' +own workflow execution. They must not be interpreted as task-to-PR attempts, +writer leases, repair counters, review records, merge guards, or canonical +lifecycle history. Run artifacts live under: @@ -553,7 +736,8 @@ safe auto-route default. `WorkflowInvocation` is documented here as the architecture boundary used by OpenLoops and neighboring packages. It is not yet a wire schema in the current catalog; add a `hasna.workflow_invocation.v1` schema only when at least two -packages need to validate the object directly at a shared boundary. +packages need to validate the object directly at a shared boundary. That +future schema would remain separate from `hasna.task_to_pr_projection.v1`. ## Enforcement Model @@ -593,6 +777,13 @@ During dual-version windows, consumers should inspect the embedded `schema` via `validateEmbeddedContract`, route accepted ids to version-specific adapters, and reject unknown schema ids before writing data or launching work. +`hasna.task_to_pr_projection.v1` is a new strict schema id; it does not widen +`hasna.work_run.v1` or any other existing v1 validator. The package source line +advances to `0.6.0` because registry/consumer evidence already showed `0.5.2` +while the repository still reported `0.5.1`. No package publish is performed +by this source change. Existing v1 inputs continue to parse unchanged, and +producers opt into the new projection explicitly by its embedded schema id. + ## Examples The `examples/` directory contains one valid fixture for every known schema plus diff --git a/examples/task-to-pr-projection.valid.json b/examples/task-to-pr-projection.valid.json new file mode 100644 index 0000000..18911d1 --- /dev/null +++ b/examples/task-to-pr-projection.valid.json @@ -0,0 +1,731 @@ +{ + "schema": "hasna.task_to_pr_projection.v1", + "id": "task_to_pr_projection:opaque-0123456789abcdef0123456789abcdef", + "createdAt": "2026-07-23T15:10:00.000Z", + "canonicalizationVersion": 2, + "identityDigest": "2ea71da0896bbae24c42ae106fd2179f46a6c327a7d40cdcd10f63ce57d0a170", + "frozenScopeDigest": "2222222222222222222222222222222222222222222222222222222222222222", + "state": "merged", + "workRunRef": { + "role": "work_run", + "authority": "codewith", + "id": "work_run:codewith:opaque-01010101010101010101010101010101", + "digest": "0101010101010101010101010101010101010101010101010101010101010101", + "redaction": "none" + }, + "rootRequestRef": { + "role": "root_request", + "authority": "todos", + "id": "root_request:todos:opaque-44444444444444444444444444444444", + "digest": "4444444444444444444444444444444444444444444444444444444444444444", + "redaction": "none" + }, + "prGroupRef": { + "role": "pr_group", + "authority": "todos", + "id": "pr_group:todos:opaque-55555555555555555555555555555555", + "digest": "5555555555555555555555555555555555555555555555555555555555555555", + "redaction": "none" + }, + "leafTaskRef": { + "role": "leaf_task", + "authority": "todos", + "id": "leaf_task:todos:opaque-66666666666666666666666666666666", + "digest": "6666666666666666666666666666666666666666666666666666666666666666", + "redaction": "none" + }, + "attempt": { + "ref": { + "role": "attempt", + "authority": "todos", + "id": "attempt:todos:opaque-02020202020202020202020202020202", + "digest": "0202020202020202020202020202020202020202020202020202020202020202", + "redaction": "none" + }, + "nonce": "attempt_nonce:opaque-11111111111111111111111111111111", + "admissionRef": { + "role": "admission", + "authority": "codewith", + "id": "admission:codewith:opaque-13131313131313131313131313131313", + "digest": "1313131313131313131313131313131313131313131313131313131313131313", + "redaction": "partial" + }, + "admissionWriterGenerationRef": { + "role": "writer_generation", + "authority": "todos", + "id": "writer_generation:todos:opaque-88888888888888888888888888888888", + "digest": "8888888888888888888888888888888888888888888888888888888888888888", + "redaction": "none" + }, + "workerActorRef": { + "role": "worker_actor", + "authority": "codewith", + "id": "worker_actor:codewith:opaque-14141414141414141414141414141414", + "digest": "1414141414141414141414141414141414141414141414141414141414141414", + "redaction": "partial" + }, + "workerRef": { + "role": "worker", + "authority": "codewith", + "id": "worker:codewith:opaque-15151515151515151515151515151515", + "digest": "1515151515151515151515151515151515151515151515151515151515151515", + "redaction": "partial" + }, + "runtimeRef": { + "role": "runtime", + "authority": "codewith", + "id": "runtime:codewith:opaque-03030303030303030303030303030303", + "digest": "0303030303030303030303030303030303030303030303030303030303030303", + "redaction": "partial" + }, + "writerGenerationRef": { + "role": "writer_generation", + "authority": "todos", + "id": "writer_generation:todos:opaque-88888888888888888888888888888888", + "digest": "8888888888888888888888888888888888888888888888888888888888888888", + "redaction": "none" + }, + "writerLeaseRef": { + "role": "writer_lease", + "authority": "repos", + "id": "writer_lease:repos:opaque-99999999999999999999999999999999", + "digest": "9999999999999999999999999999999999999999999999999999999999999999", + "redaction": "partial" + }, + "writerFenceRef": { + "role": "writer_fence", + "authority": "repos", + "id": "writer_fence:repos:opaque-04040404040404040404040404040404", + "digest": "0404040404040404040404040404040404040404040404040404040404040404", + "redaction": "full" + }, + "providerProfileRef": { + "role": "provider_profile", + "authority": "codewith", + "id": "provider_profile:codewith:opaque-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "redaction": "full" + }, + "providerRouteRef": { + "role": "provider_route", + "authority": "codewith", + "id": "provider_route:codewith:opaque-cccccccccccccccccccccccccccccccc", + "digest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "redaction": "partial" + } + }, + "repository": { + "repoRef": { + "role": "repo", + "authority": "repos", + "id": "repo:repos:opaque-dddddddddddddddddddddddddddddddd", + "digest": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "redaction": "none" + }, + "worktreeRef": { + "role": "worktree", + "authority": "repos", + "id": "worktree:repos:opaque-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "digest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "redaction": "partial" + }, + "branchRef": { + "role": "branch", + "authority": "repos", + "id": "branch:repos:opaque-ffffffffffffffffffffffffffffffff", + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "redaction": "none" + }, + "baseHead": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "branchHead": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + } + }, + "events": { + "streamRef": { + "role": "event_stream", + "authority": "todos", + "id": "event_stream:todos:opaque-11111111111111111111111111111111", + "digest": "1111111111111111111111111111111111111111111111111111111111111111", + "redaction": "none" + }, + "replayCursorRef": { + "role": "replay_cursor", + "authority": "todos", + "id": "replay_cursor:todos:opaque-22222222222222222222222222222222", + "digest": "2222222222222222222222222222222222222222222222222222222222222222", + "redaction": "none" + }, + "sequence": 7, + "prefixDigest": "3333333333333333333333333333333333333333333333333333333333333333" + }, + "pullRequestRef": { + "role": "pull_request", + "authority": "todos", + "id": "pull_request:todos:opaque-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "digest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "redaction": "none" + }, + "exactHead": { + "pullRequestRef": { + "role": "pull_request", + "authority": "todos", + "id": "pull_request:todos:opaque-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "digest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "redaction": "none" + }, + "remoteBranchRef": { + "role": "branch", + "authority": "repos", + "id": "branch:repos:opaque-ffffffffffffffffffffffffffffffff", + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "redaction": "none" + }, + "expectedBase": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "providerPullRequestBase": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "localHead": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + }, + "remoteHead": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + }, + "providerPullRequestHead": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + }, + "equalityProofRef": { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-ffffffffffffffffffffffffffffffff", + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "redaction": "none" + }, + "ciProofBundleRefs": [ + { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-00000000000000000000000000000000", + "digest": "0000000000000000000000000000000000000000000000000000000000000000", + "redaction": "none" + } + ], + "verifiedAt": "2026-07-23T15:10:00.000Z" + }, + "reviews": [ + { + "ref": { + "role": "review", + "authority": "review", + "id": "review:review:opaque-66666666666666666666666666666666", + "digest": "6666666666666666666666666666666666666666666666666666666666666666", + "redaction": "none" + }, + "pullRequestRef": { + "role": "pull_request", + "authority": "todos", + "id": "pull_request:todos:opaque-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "digest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "redaction": "none" + }, + "base": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "head": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + }, + "reviewerRef": { + "role": "reviewer", + "authority": "review", + "id": "reviewer:review:opaque-44444444444444444444444444444444", + "digest": "4444444444444444444444444444444444444444444444444444444444444444", + "redaction": "none" + }, + "reviewRunRef": { + "role": "review_run", + "authority": "review", + "id": "review_run:review:opaque-55555555555555555555555555555555", + "digest": "5555555555555555555555555555555555555555555555555555555555555555", + "redaction": "none" + }, + "proofBundleRef": { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-11111111111111111111111111111111", + "digest": "1111111111111111111111111111111111111111111111111111111111111111", + "redaction": "none" + }, + "verdict": "approved", + "reviewedAt": "2026-07-23T15:10:00.000Z" + } + ], + "repair": { + "ref": { + "role": "repair_cycle", + "authority": "todos", + "id": "repair_cycle:todos:opaque-44444444444444444444444444444444", + "digest": "4444444444444444444444444444444444444444444444444444444444444444", + "redaction": "none" + }, + "cycle": 0, + "cap": 2, + "exhausted": false + }, + "merge": { + "guard": { + "ref": { + "role": "merge_guard", + "authority": "todos", + "id": "merge_guard:todos:opaque-77777777777777777777777777777777", + "digest": "7777777777777777777777777777777777777777777777777777777777777777", + "redaction": "none" + }, + "pullRequestRef": { + "role": "pull_request", + "authority": "todos", + "id": "pull_request:todos:opaque-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "digest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "redaction": "none" + }, + "expectedBase": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "expectedHead": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + }, + "reviewRefs": [ + { + "role": "review", + "authority": "review", + "id": "review:review:opaque-66666666666666666666666666666666", + "digest": "6666666666666666666666666666666666666666666666666666666666666666", + "redaction": "none" + } + ], + "proofBundleRefs": [ + { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-11111111111111111111111111111111", + "digest": "1111111111111111111111111111111111111111111111111111111111111111", + "redaction": "none" + }, + { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-ffffffffffffffffffffffffffffffff", + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "redaction": "none" + }, + { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-00000000000000000000000000000000", + "digest": "0000000000000000000000000000000000000000000000000000000000000000", + "redaction": "none" + } + ], + "operatorRef": { + "role": "merge_operator", + "authority": "merge_provider", + "id": "merge_operator:merge_provider:opaque-88888888888888888888888888888888", + "digest": "8888888888888888888888888888888888888888888888888888888888888888", + "redaction": "partial" + }, + "operatorRunRef": { + "role": "merge_operator_run", + "authority": "merge_provider", + "id": "merge_operator_run:merge_provider:opaque-99999999999999999999999999999999", + "digest": "9999999999999999999999999999999999999999999999999999999999999999", + "redaction": "partial" + }, + "providerGuardReceiptRef": { + "role": "merge_guard_receipt", + "authority": "merge_provider", + "id": "merge_guard_receipt:merge_provider:opaque-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "redaction": "full" + }, + "mechanism": "compare_and_swap", + "decision": "consumed", + "evaluatedAt": "2026-07-23T15:10:00.000Z" + }, + "outcome": { + "ref": { + "role": "merge_outcome", + "authority": "merge_provider", + "id": "merge_outcome:merge_provider:opaque-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "redaction": "partial" + }, + "guardRef": { + "role": "merge_guard", + "authority": "todos", + "id": "merge_guard:todos:opaque-77777777777777777777777777777777", + "digest": "7777777777777777777777777777777777777777777777777777777777777777", + "redaction": "none" + }, + "pullRequestRef": { + "role": "pull_request", + "authority": "todos", + "id": "pull_request:todos:opaque-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "digest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "redaction": "none" + }, + "expectedBase": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "observedBase": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "expectedHead": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + }, + "observedHead": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + }, + "status": "merged", + "mergeCommitRef": { + "role": "commit", + "authority": "repos", + "id": "commit:repos:opaque-cccccccccccccccccccccccccccccccc", + "digest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "redaction": "none" + }, + "finishedAt": "2026-07-23T15:10:00.000Z", + "evidenceRefs": [ + { + "id": "evidence:opaque-dddddddddddddddddddddddddddddddd", + "digest": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "redaction": "full" + } + ] + } + }, + "terminalDispositionRef": { + "role": "terminal_disposition", + "authority": "todos", + "id": "terminal_disposition:todos:opaque-16161616161616161616161616161616", + "digest": "1616161616161616161616161616161616161616161616161616161616161616", + "redaction": "none" + }, + "provenanceLedger": [ + { + "category": "projection_id", + "projectionId": "task_to_pr_projection:opaque-0123456789abcdef0123456789abcdef" + }, + { + "category": "work_run", + "ref": { + "role": "work_run", + "authority": "codewith", + "id": "work_run:codewith:opaque-01010101010101010101010101010101", + "digest": "0101010101010101010101010101010101010101010101010101010101010101", + "redaction": "none" + } + }, + { + "category": "attempt", + "ref": { + "role": "attempt", + "authority": "todos", + "id": "attempt:todos:opaque-02020202020202020202020202020202", + "digest": "0202020202020202020202020202020202020202020202020202020202020202", + "redaction": "none" + } + }, + { + "category": "admission", + "ref": { + "role": "admission", + "authority": "codewith", + "id": "admission:codewith:opaque-13131313131313131313131313131313", + "digest": "1313131313131313131313131313131313131313131313131313131313131313", + "redaction": "partial" + } + }, + { + "category": "worker_actor", + "ref": { + "role": "worker_actor", + "authority": "codewith", + "id": "worker_actor:codewith:opaque-14141414141414141414141414141414", + "digest": "1414141414141414141414141414141414141414141414141414141414141414", + "redaction": "partial" + } + }, + { + "category": "worker_assignment", + "ref": { + "role": "worker", + "authority": "codewith", + "id": "worker:codewith:opaque-15151515151515151515151515151515", + "digest": "1515151515151515151515151515151515151515151515151515151515151515", + "redaction": "partial" + } + }, + { + "category": "attempt_nonce", + "nonce": "attempt_nonce:opaque-11111111111111111111111111111111" + }, + { + "category": "runtime", + "ref": { + "role": "runtime", + "authority": "codewith", + "id": "runtime:codewith:opaque-03030303030303030303030303030303", + "digest": "0303030303030303030303030303030303030303030303030303030303030303", + "redaction": "partial" + } + }, + { + "category": "writer_generation", + "ref": { + "role": "writer_generation", + "authority": "todos", + "id": "writer_generation:todos:opaque-88888888888888888888888888888888", + "digest": "8888888888888888888888888888888888888888888888888888888888888888", + "redaction": "none" + } + }, + { + "category": "writer_lease", + "ref": { + "role": "writer_lease", + "authority": "repos", + "id": "writer_lease:repos:opaque-99999999999999999999999999999999", + "digest": "9999999999999999999999999999999999999999999999999999999999999999", + "redaction": "partial" + } + }, + { + "category": "writer_fence", + "ref": { + "role": "writer_fence", + "authority": "repos", + "id": "writer_fence:repos:opaque-04040404040404040404040404040404", + "digest": "0404040404040404040404040404040404040404040404040404040404040404", + "redaction": "full" + } + }, + { + "category": "provider_profile", + "ref": { + "role": "provider_profile", + "authority": "codewith", + "id": "provider_profile:codewith:opaque-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "redaction": "full" + } + }, + { + "category": "provider_route", + "ref": { + "role": "provider_route", + "authority": "codewith", + "id": "provider_route:codewith:opaque-cccccccccccccccccccccccccccccccc", + "digest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "redaction": "partial" + } + }, + { + "category": "replay_cursor", + "ref": { + "role": "replay_cursor", + "authority": "todos", + "id": "replay_cursor:todos:opaque-22222222222222222222222222222222", + "digest": "2222222222222222222222222222222222222222222222222222222222222222", + "redaction": "none" + } + }, + { + "category": "replay_prefix", + "sequence": 7, + "prefixDigest": "3333333333333333333333333333333333333333333333333333333333333333" + }, + { + "category": "repair_state", + "ref": { + "role": "repair_cycle", + "authority": "todos", + "id": "repair_cycle:todos:opaque-44444444444444444444444444444444", + "digest": "4444444444444444444444444444444444444444444444444444444444444444", + "redaction": "none" + } + }, + { + "category": "equality_proof", + "ref": { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-ffffffffffffffffffffffffffffffff", + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "redaction": "none" + }, + "base": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "head": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + } + }, + { + "category": "ci_proof", + "ref": { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-00000000000000000000000000000000", + "digest": "0000000000000000000000000000000000000000000000000000000000000000", + "redaction": "none" + }, + "base": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "head": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + } + }, + { + "category": "review_proof", + "ref": { + "role": "proof_bundle", + "authority": "review", + "id": "proof_bundle:review:opaque-11111111111111111111111111111111", + "digest": "1111111111111111111111111111111111111111111111111111111111111111", + "redaction": "none" + }, + "base": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "head": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + } + }, + { + "category": "review_record", + "ref": { + "role": "review", + "authority": "review", + "id": "review:review:opaque-66666666666666666666666666666666", + "digest": "6666666666666666666666666666666666666666666666666666666666666666", + "redaction": "none" + }, + "base": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "head": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + } + }, + { + "category": "review_run", + "ref": { + "role": "review_run", + "authority": "review", + "id": "review_run:review:opaque-55555555555555555555555555555555", + "digest": "5555555555555555555555555555555555555555555555555555555555555555", + "redaction": "none" + }, + "base": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "head": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + } + }, + { + "category": "merge_guard", + "ref": { + "role": "merge_guard", + "authority": "todos", + "id": "merge_guard:todos:opaque-77777777777777777777777777777777", + "digest": "7777777777777777777777777777777777777777777777777777777777777777", + "redaction": "none" + } + }, + { + "category": "provider_guard_receipt", + "ref": { + "role": "merge_guard_receipt", + "authority": "merge_provider", + "id": "merge_guard_receipt:merge_provider:opaque-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "redaction": "full" + }, + "base": { + "algorithm": "sha1", + "value": "1111111111111111111111111111111111111111" + }, + "head": { + "algorithm": "sha1", + "value": "2222222222222222222222222222222222222222" + } + }, + { + "category": "terminal_disposition", + "ref": { + "role": "terminal_disposition", + "authority": "todos", + "id": "terminal_disposition:todos:opaque-16161616161616161616161616161616", + "digest": "1616161616161616161616161616161616161616161616161616161616161616", + "redaction": "none" + } + } + ], + "adapterExtensions": [ + { + "mode": "local", + "schema": "hasna.task_to_pr_adapter_extension.v1", + "ref": { + "role": "adapter_extension", + "authority": "adapter", + "id": "adapter_extension:adapter:opaque-55555555555555555555555555555555", + "digest": "5555555555555555555555555555555555555555555555555555555555555555", + "redaction": "partial" + }, + "digest": "6666666666666666666666666666666666666666666666666666666666666666" + }, + { + "mode": "cloud", + "schema": "hasna.task_to_pr_adapter_extension.v1", + "ref": { + "role": "adapter_extension", + "authority": "adapter", + "id": "adapter_extension:adapter:opaque-77777777777777777777777777777777", + "digest": "7777777777777777777777777777777777777777777777777777777777777777", + "redaction": "full" + }, + "digest": "8888888888888888888888888888888888888888888888888888888888888888" + } + ], + "evidenceRefs": [] +} diff --git a/package.json b/package.json index 23dfeb7..2247617 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hasna/contracts", - "version": "0.5.3", + "version": "0.6.0", "description": "Shared schemas and validators for Hasna open-source agent infrastructure contracts.", "type": "module", "license": "Apache-2.0", diff --git a/src/schemas.ts b/src/schemas.ts index 072347a..5308d67 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,13 +1,15 @@ +import { createHash } from "node:crypto"; import { z } from "zod"; export const CONTRACTS_PACKAGE_NAME = "@hasna/contracts"; -export const CONTRACTS_PACKAGE_VERSION = "0.5.3"; +export const CONTRACTS_PACKAGE_VERSION = "0.6.0"; export const SCHEMA_IDS = { actorRef: "hasna.actor_ref.v1", resourceRef: "hasna.resource_ref.v1", evidenceRef: "hasna.evidence_ref.v1", workRun: "hasna.work_run.v1", + taskToPrProjection: "hasna.task_to_pr_projection.v1", decisionEnvelope: "hasna.decision_envelope.v1", costEstimate: "hasna.cost_estimate.v1", capabilityCard: "hasna.capability_card.v1", @@ -2030,6 +2032,2891 @@ export const WorkRunSchema = contractBaseSchema(SCHEMA_IDS.workRun) }); export type WorkRun = z.infer; +export const TASK_TO_PR_ROLE_AUTHORITIES = Object.freeze({ + work_run: Object.freeze(["codewith"] as const), + root_request: Object.freeze(["todos"] as const), + pr_group: Object.freeze(["todos"] as const), + leaf_task: Object.freeze(["todos"] as const), + attempt: Object.freeze(["todos"] as const), + writer_generation: Object.freeze(["todos"] as const), + writer_lease: Object.freeze(["repos"] as const), + writer_fence: Object.freeze(["repos"] as const), + provider_profile: Object.freeze(["codewith"] as const), + provider_route: Object.freeze(["codewith"] as const), + admission: Object.freeze(["codewith"] as const), + worker_actor: Object.freeze(["codewith"] as const), + worker: Object.freeze(["codewith"] as const), + runtime: Object.freeze(["codewith"] as const), + repo: Object.freeze(["repos"] as const), + worktree: Object.freeze(["repos"] as const), + branch: Object.freeze(["repos"] as const), + event_stream: Object.freeze(["todos"] as const), + replay_cursor: Object.freeze(["todos"] as const), + handoff: Object.freeze(["todos"] as const), + pull_request: Object.freeze(["todos"] as const), + commit: Object.freeze(["repos"] as const), + review: Object.freeze(["review"] as const), + reviewer: Object.freeze(["review"] as const), + review_run: Object.freeze(["review"] as const), + proof_bundle: Object.freeze(["review"] as const), + repair_cycle: Object.freeze(["todos"] as const), + merge_guard: Object.freeze(["todos"] as const), + merge_operator: Object.freeze(["merge_provider"] as const), + merge_operator_run: Object.freeze(["merge_provider"] as const), + merge_guard_receipt: Object.freeze(["merge_provider"] as const), + merge_outcome: Object.freeze(["merge_provider"] as const), + recovery: Object.freeze(["todos"] as const), + cancellation: Object.freeze(["todos"] as const), + cleanup_eligibility: Object.freeze(["repos"] as const), + cleanup_outcome: Object.freeze(["repos"] as const), + rollback_plan: Object.freeze(["todos"] as const), + rollback_outcome: Object.freeze(["repos"] as const), + terminal_disposition: Object.freeze(["todos"] as const), + openloops_invocation: Object.freeze(["openloops"] as const), + adapter_extension: Object.freeze(["adapter"] as const) +}); + +export const TaskToPrRefRoleSchema = z.enum([ + "work_run", + "root_request", + "pr_group", + "leaf_task", + "attempt", + "writer_generation", + "writer_lease", + "writer_fence", + "provider_profile", + "provider_route", + "admission", + "worker_actor", + "worker", + "runtime", + "repo", + "worktree", + "branch", + "event_stream", + "replay_cursor", + "handoff", + "pull_request", + "commit", + "review", + "reviewer", + "review_run", + "proof_bundle", + "repair_cycle", + "merge_guard", + "merge_operator", + "merge_operator_run", + "merge_guard_receipt", + "merge_outcome", + "recovery", + "cancellation", + "cleanup_eligibility", + "cleanup_outcome", + "rollback_plan", + "rollback_outcome", + "terminal_disposition", + "openloops_invocation", + "adapter_extension" +]); +export type TaskToPrRefRole = z.infer; + +export const TaskToPrAuthoritySchema = z.enum([ + "todos", + "codewith", + "repos", + "review", + "merge_provider", + "openloops", + "adapter" +]); +export type TaskToPrAuthority = z.infer; + +const LowerSha256DigestSchema = z.string().regex(/^[a-f0-9]{64}$/); +const OpaqueTaskToPrIdSchema = z.string().trim().min(3).max(256); +const NonsemanticOpaqueSuffixPattern = /^[a-f0-9]{32}$/; +export function deriveTaskToPrRefId( + role: TaskToPrRefRole, + authority: TaskToPrAuthority, + digest: string +): string { + return `${role}:${authority}:opaque-${digest.slice(0, 32)}`; +} +export function deriveTaskToPrEvidenceId(digest: string): string { + return `evidence:opaque-${digest.slice(0, 32)}`; +} +const TaskToPrProjectionIdSchema = OpaqueTaskToPrIdSchema.refine( + (value) => { + const prefix = "task_to_pr_projection:opaque-"; + const suffix = value.startsWith(prefix) ? value.slice(prefix.length) : ""; + return NonsemanticOpaqueSuffixPattern.test(suffix); + }, + "Projection ids must use a nonsemantic 128-bit lowercase hexadecimal surrogate" +); +const TaskToPrAttemptNonceSchema = OpaqueTaskToPrIdSchema.refine( + (value) => { + const prefix = "attempt_nonce:opaque-"; + const suffix = value.startsWith(prefix) ? value.slice(prefix.length) : ""; + return NonsemanticOpaqueSuffixPattern.test(suffix); + }, + "Attempt nonces must use a nonsemantic 128-bit lowercase hexadecimal surrogate" +); +const SensitiveTaskToPrRoles = new Set([ + "writer_lease", + "writer_fence", + "provider_profile", + "provider_route", + "admission", + "worker_actor", + "worker", + "runtime", + "worktree", + "merge_operator", + "merge_operator_run", + "merge_guard_receipt", + "merge_outcome", + "openloops_invocation", + "adapter_extension" +]); +export const TaskToPrRefSchema = z + .object({ + role: TaskToPrRefRoleSchema, + authority: TaskToPrAuthoritySchema, + id: OpaqueTaskToPrIdSchema, + digest: LowerSha256DigestSchema, + redaction: z.enum(["none", "partial", "full"]) + }) + .strict() + .superRefine((value, ctx) => { + const allowedAuthorities = TASK_TO_PR_ROLE_AUTHORITIES[value.role] as readonly TaskToPrAuthority[]; + if (!allowedAuthorities.includes(value.authority)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value.role} refs must be owned by ${allowedAuthorities.join(" or ")}`, + path: ["authority"] + }); + } + if (SensitiveTaskToPrRoles.has(value.role) && value.redaction === "none") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value.role} refs must be redacted and cannot carry a raw locator or credential`, + path: ["redaction"] + }); + } + const expectedId = deriveTaskToPrRefId(value.role, value.authority, value.digest); + if (value.id !== expectedId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Reference ids must be nonsemantic authority-bound surrogates derived from the canonical role, authority, and owner-record digest", + path: ["id"] + }); + } + }); +export type TaskToPrRef = z.infer; + +export const TaskToPrEvidenceRefSchema = z + .object({ + id: OpaqueTaskToPrIdSchema, + digest: LowerSha256DigestSchema, + redaction: z.enum(["partial", "full"]) + }) + .strict() + .superRefine((value, ctx) => { + if (value.id !== deriveTaskToPrEvidenceId(value.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Evidence ids must be nonsemantic owner-resolvable surrogates derived from their canonical digest", + path: ["id"] + }); + } + }); +export type TaskToPrEvidenceRef = z.infer; + +function requireDistinctTaskToPrEvidenceRefs( + stopEvidenceRef: TaskToPrEvidenceRef, + leaseRevocationEvidenceRef: TaskToPrEvidenceRef, + ctx: z.RefinementCtx, + path: (string | number)[] +): void { + if ( + stopEvidenceRef.id === leaseRevocationEvidenceRef.id || + stopEvidenceRef.digest === leaseRevocationEvidenceRef.digest + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Stop and lease-revocation facts require distinct evidence identities and digests", + path + }); + } +} + +function taskToPrRefFor(role: TaskToPrRefRole) { + return TaskToPrRefSchema.refine((value) => value.role === role, { + message: `Reference must use role ${role}`, + path: ["role"] + }); +} + +function sameTaskToPrRef(left: TaskToPrRef, right: TaskToPrRef): boolean { + return ( + left.role === right.role && + left.authority === right.authority && + left.id === right.id && + left.digest === right.digest && + left.redaction === right.redaction + ); +} + +function sameTaskToPrCanonicalRefId(left: TaskToPrRef, right: TaskToPrRef): boolean { + return left.role === right.role && left.authority === right.authority && left.id === right.id; +} + +function requireFreshTaskToPrRef( + prior: TaskToPrRef, + successor: TaskToPrRef, + ctx: z.RefinementCtx, + path: (string | number)[], + label: string +): void { + if (sameTaskToPrCanonicalRefId(prior, successor)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${label} requires a fresh canonical role/authority/id`, + path + }); + } + if (prior.digest === successor.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${label} requires a fresh canonical digest`, + path + }); + } +} + +function taskToPrCanonicalRefKey(ref: TaskToPrRef): string { + return `${ref.role}\u0000${ref.authority}\u0000${ref.id}`; +} + +function sameGitObjectId( + left: z.infer, + right: z.infer +): boolean { + return left.algorithm === right.algorithm && left.value === right.value; +} + +function sameTaskToPrMergeGuardLineageFacts( + left: z.infer, + right: z.infer +): boolean { + return ( + sameTaskToPrRef(left.pullRequestRef, right.pullRequestRef) && + sameGitObjectId(left.expectedBase, right.expectedBase) && + sameGitObjectId(left.expectedHead, right.expectedHead) && + JSON.stringify(left.reviewRefs) === JSON.stringify(right.reviewRefs) && + JSON.stringify(left.proofBundleRefs) === JSON.stringify(right.proofBundleRefs) && + sameTaskToPrRef(left.operatorRef, right.operatorRef) && + sameTaskToPrRef(left.operatorRunRef, right.operatorRunRef) && + sameTaskToPrRef(left.providerGuardReceiptRef, right.providerGuardReceiptRef) && + left.mechanism === right.mechanism + ); +} + +export const TaskToPrGitObjectIdSchema = z + .object({ + algorithm: z.enum(["sha1", "sha256"]), + value: z.string().regex(/^[a-f0-9]+$/) + }) + .strict() + .superRefine((value, ctx) => { + const requiredLength = value.algorithm === "sha1" ? 40 : 64; + if (value.value.length !== requiredLength) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value.algorithm} object ids must contain exactly ${requiredLength} lowercase hex characters`, + path: ["value"] + }); + } + }); +export type TaskToPrGitObjectId = z.infer; + +export interface TaskToPrBindingV1Input { + canonicalizationVersion: 1; + rootRequestRef: TaskToPrRef; + prGroupRef: TaskToPrRef; + leafTaskRef: TaskToPrRef; + repoRef: TaskToPrRef; + baseHead: TaskToPrGitObjectId; + frozenScopeDigest: string; +} + +export interface TaskToPrBindingV2Input { + canonicalizationVersion: 2; + rootRequestRef: TaskToPrRef; + prGroupRef: TaskToPrRef; + leafTaskRef: TaskToPrRef; + repoRef: TaskToPrRef; + worktreeRef: TaskToPrRef; + branchRef: TaskToPrRef; + baseHead: TaskToPrGitObjectId; + frozenScopeDigest: string; +} + +export type TaskToPrBindingInput = TaskToPrBindingV1Input | TaskToPrBindingV2Input; + +export function deriveTaskToPrIdentityDigest(input: TaskToPrBindingInput): string { + if (input.canonicalizationVersion === 1) { + const legacyCanonicalBinding = JSON.stringify([ + "hasna.task_to_pr_projection.binding.v1", + input.canonicalizationVersion, + input.rootRequestRef.id, + input.rootRequestRef.digest, + input.prGroupRef.id, + input.prGroupRef.digest, + input.leafTaskRef.id, + input.leafTaskRef.digest, + input.repoRef.id, + input.repoRef.digest, + input.baseHead.algorithm, + input.baseHead.value, + input.frozenScopeDigest + ]); + return createHash("sha256").update(legacyCanonicalBinding, "utf8").digest("hex"); + } + const canonicalBinding = JSON.stringify([ + "hasna.task_to_pr_projection.binding.v2", + input.canonicalizationVersion, + ...[input.rootRequestRef, input.prGroupRef, input.leafTaskRef, input.repoRef, input.worktreeRef, input.branchRef] + .flatMap((ref) => [ref.role, ref.authority, ref.id, ref.digest]), + input.baseHead.algorithm, + input.baseHead.value, + input.frozenScopeDigest + ]); + return createHash("sha256").update(canonicalBinding, "utf8").digest("hex"); +} + +export const TaskToPrAttemptSchema = z + .object({ + ref: taskToPrRefFor("attempt"), + nonce: TaskToPrAttemptNonceSchema, + admissionRef: taskToPrRefFor("admission"), + admissionWriterGenerationRef: taskToPrRefFor("writer_generation"), + workerActorRef: taskToPrRefFor("worker_actor"), + workerRef: taskToPrRefFor("worker"), + runtimeRef: taskToPrRefFor("runtime"), + writerGenerationRef: taskToPrRefFor("writer_generation"), + writerLeaseRef: taskToPrRefFor("writer_lease"), + writerFenceRef: taskToPrRefFor("writer_fence"), + providerProfileRef: taskToPrRefFor("provider_profile"), + providerRouteRef: taskToPrRefFor("provider_route") + }) + .strict(); +export type TaskToPrAttempt = z.infer; + +export const TaskToPrRepositoryBindingSchema = z + .object({ + repoRef: taskToPrRefFor("repo"), + worktreeRef: taskToPrRefFor("worktree"), + branchRef: taskToPrRefFor("branch"), + baseHead: TaskToPrGitObjectIdSchema, + branchHead: TaskToPrGitObjectIdSchema + }) + .strict(); +export type TaskToPrRepositoryBinding = z.infer; + +export const TaskToPrEventCursorSchema = z + .object({ + streamRef: taskToPrRefFor("event_stream"), + replayCursorRef: taskToPrRefFor("replay_cursor"), + sequence: z.number().int().safe().nonnegative(), + prefixDigest: LowerSha256DigestSchema + }) + .strict(); +export type TaskToPrEventCursor = z.infer; + +export const TaskToPrHandoffSchema = z + .object({ + ref: taskToPrRefFor("handoff"), + previousAttemptRef: taskToPrRefFor("attempt"), + nextAttemptRef: taskToPrRefFor("attempt"), + previousWriterGenerationRef: taskToPrRefFor("writer_generation"), + nextWriterGenerationRef: taskToPrRefFor("writer_generation"), + stoppedWorkRunRef: taskToPrRefFor("work_run"), + stopEvidenceRef: TaskToPrEvidenceRefSchema, + leaseRevocationEvidenceRef: TaskToPrEvidenceRefSchema + }) + .strict() + .superRefine((value, ctx) => { + requireFreshTaskToPrRef( + value.previousAttemptRef, + value.nextAttemptRef, + ctx, + ["nextAttemptRef"], + "Handoff attempt rotation" + ); + requireFreshTaskToPrRef( + value.previousWriterGenerationRef, + value.nextWriterGenerationRef, + ctx, + ["nextWriterGenerationRef"], + "Handoff writer-generation rotation" + ); + requireDistinctTaskToPrEvidenceRefs( + value.stopEvidenceRef, + value.leaseRevocationEvidenceRef, + ctx, + ["leaseRevocationEvidenceRef"] + ); + }); +export type TaskToPrHandoff = z.infer; + +export const TaskToPrReviewBindingSchema = z + .object({ + ref: taskToPrRefFor("review"), + pullRequestRef: taskToPrRefFor("pull_request"), + base: TaskToPrGitObjectIdSchema, + head: TaskToPrGitObjectIdSchema, + reviewerRef: taskToPrRefFor("reviewer"), + reviewRunRef: taskToPrRefFor("review_run"), + proofBundleRef: taskToPrRefFor("proof_bundle"), + verdict: z.enum(["approved", "changes_requested", "blocked"]), + reviewedAt: TimestampSchema + }) + .strict(); +export type TaskToPrReviewBinding = z.infer; + +export const TaskToPrExactHeadBindingSchema = z + .object({ + pullRequestRef: taskToPrRefFor("pull_request"), + remoteBranchRef: taskToPrRefFor("branch"), + expectedBase: TaskToPrGitObjectIdSchema, + providerPullRequestBase: TaskToPrGitObjectIdSchema, + localHead: TaskToPrGitObjectIdSchema, + remoteHead: TaskToPrGitObjectIdSchema, + providerPullRequestHead: TaskToPrGitObjectIdSchema, + equalityProofRef: taskToPrRefFor("proof_bundle"), + ciProofBundleRefs: z.array(taskToPrRefFor("proof_bundle")).min(1), + verifiedAt: TimestampSchema + }) + .strict() + .superRefine((value, ctx) => { + if (!sameGitObjectId(value.expectedBase, value.providerPullRequestBase)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Expected and provider-observed pull-request bases must be exactly equal", + path: ["providerPullRequestBase"] + }); + } + if ( + !sameGitObjectId(value.localHead, value.remoteHead) || + !sameGitObjectId(value.localHead, value.providerPullRequestHead) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Local, remote, and provider pull-request heads must be exactly equal", + path: ["providerPullRequestHead"] + }); + } + const proofKeys = value.ciProofBundleRefs.map(taskToPrCanonicalRefKey); + if (new Set(proofKeys).size !== proofKeys.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "CI proof bundle refs must have unique canonical identities", + path: ["ciProofBundleRefs"] + }); + } + const proofDigests = value.ciProofBundleRefs.map((ref) => ref.digest); + if (new Set(proofDigests).size !== proofDigests.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "CI proof bundle refs must have unique canonical digests", + path: ["ciProofBundleRefs"] + }); + } + if (value.ciProofBundleRefs.some((ref) => sameTaskToPrCanonicalRefId(ref, value.equalityProofRef))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Head-equality and CI proof refs must have distinct canonical identities", + path: ["ciProofBundleRefs"] + }); + } + if (value.ciProofBundleRefs.some((ref) => ref.digest === value.equalityProofRef.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Head-equality and CI proof refs must have distinct canonical digests", + path: ["ciProofBundleRefs"] + }); + } + }); +export type TaskToPrExactHeadBinding = z.infer; + +export const TaskToPrRepairStateSchema = z + .object({ + ref: taskToPrRefFor("repair_cycle"), + cycle: z.number().int().min(0).max(2), + cap: z.literal(2), + exhausted: z.boolean(), + latestRepairRef: taskToPrRefFor("repair_cycle").optional() + }) + .strict() + .superRefine((value, ctx) => { + if (value.exhausted !== (value.cycle === value.cap)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Repair exhaustion must equal the cumulative cycle cap", + path: ["exhausted"] + }); + } + if (value.cycle === 0 && value.latestRepairRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cycle zero cannot reference a repair", + path: ["latestRepairRef"] + }); + } + if (value.cycle > 0 && !value.latestRepairRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Non-zero repair state requires the latest immutable repair ref", + path: ["latestRepairRef"] + }); + } + if (value.latestRepairRef && sameTaskToPrCanonicalRefId(value.ref, value.latestRepairRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Repair-state and latest-repair refs must be distinct canonical records", + path: ["latestRepairRef"] + }); + } + if (value.latestRepairRef && value.ref.digest === value.latestRepairRef.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Repair-state and latest-repair refs must have distinct canonical digests", + path: ["latestRepairRef"] + }); + } + }); +export type TaskToPrRepairState = z.infer; + +export const TaskToPrMergeGuardSchema = z + .object({ + ref: taskToPrRefFor("merge_guard"), + pullRequestRef: taskToPrRefFor("pull_request"), + expectedBase: TaskToPrGitObjectIdSchema, + expectedHead: TaskToPrGitObjectIdSchema, + reviewRefs: z.array(taskToPrRefFor("review")).min(1), + proofBundleRefs: z.array(taskToPrRefFor("proof_bundle")).min(1), + operatorRef: taskToPrRefFor("merge_operator"), + operatorRunRef: taskToPrRefFor("merge_operator_run"), + providerGuardReceiptRef: taskToPrRefFor("merge_guard_receipt"), + mechanism: z.enum(["compare_and_swap", "queue_expected_head"]), + decision: z.enum(["eligible", "denied", "consumed", "revoked"]), + evaluatedAt: TimestampSchema + }) + .strict() + .superRefine((value, ctx) => { + const uniqueReviews = new Set(value.reviewRefs.map((ref) => ref.id)); + if (uniqueReviews.size !== value.reviewRefs.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guard review refs must be unique", + path: ["reviewRefs"] + }); + } + const uniqueProofs = new Set(value.proofBundleRefs.map(taskToPrCanonicalRefKey)); + if (uniqueProofs.size !== value.proofBundleRefs.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guard proof refs must have unique canonical identities", + path: ["proofBundleRefs"] + }); + } + const uniqueProofDigests = new Set(value.proofBundleRefs.map((ref) => ref.digest)); + if (uniqueProofDigests.size !== value.proofBundleRefs.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guard proof refs must have unique canonical digests", + path: ["proofBundleRefs"] + }); + } + }); +export type TaskToPrMergeGuard = z.infer; + +export const TaskToPrMergeOutcomeSchema = z + .object({ + ref: taskToPrRefFor("merge_outcome"), + guardRef: taskToPrRefFor("merge_guard"), + pullRequestRef: taskToPrRefFor("pull_request"), + expectedBase: TaskToPrGitObjectIdSchema, + observedBase: TaskToPrGitObjectIdSchema, + expectedHead: TaskToPrGitObjectIdSchema, + observedHead: TaskToPrGitObjectIdSchema, + status: z.enum(["merged", "closed_unmerged", "refused", "head_drift", "base_drift"]), + mergeCommitRef: taskToPrRefFor("commit").optional(), + finishedAt: TimestampSchema, + evidenceRefs: z.array(TaskToPrEvidenceRefSchema).min(1) + }) + .strict() + .superRefine((value, ctx) => { + const baseMatches = sameGitObjectId(value.expectedBase, value.observedBase); + const headMatches = sameGitObjectId(value.expectedHead, value.observedHead); + if (value.status === "merged") { + if (!baseMatches || !headMatches) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merged outcomes require observed base and head to equal the guarded values", + path: [!baseMatches ? "observedBase" : "observedHead"] + }); + } + if (!value.mergeCommitRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merged outcomes require an immutable merge commit ref", + path: ["mergeCommitRef"] + }); + } + } else if (value.mergeCommitRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Unmerged outcomes cannot claim a merge commit", + path: ["mergeCommitRef"] + }); + } + if (value.status === "head_drift" && headMatches) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Head-drift outcomes require distinct expected and observed heads", + path: ["observedHead"] + }); + } + if (value.status === "head_drift" && !baseMatches) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Head-drift outcomes cannot also carry an unclassified base drift", + path: ["observedBase"] + }); + } + if (value.status === "base_drift" && baseMatches) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Base-drift outcomes require distinct expected and observed bases", + path: ["observedBase"] + }); + } + if (value.status === "base_drift" && !headMatches) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Base-drift outcomes cannot also carry an unclassified head drift", + path: ["observedHead"] + }); + } + if (!headMatches && value.status !== "head_drift") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Only a head_drift outcome may record an observed head that differs from the expected head", + path: ["observedHead"] + }); + } + if (!baseMatches && value.status !== "base_drift") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Only a base_drift outcome may record an observed base that differs from the expected base", + path: ["observedBase"] + }); + } + }); +export type TaskToPrMergeOutcome = z.infer; + +export const TaskToPrMergeStateSchema = z + .object({ + guard: TaskToPrMergeGuardSchema, + outcome: TaskToPrMergeOutcomeSchema.optional() + }) + .strict(); +export type TaskToPrMergeState = z.infer; + +export const TaskToPrRecoverySchema = z + .object({ + ref: taskToPrRefFor("recovery"), + priorAttemptRef: taskToPrRefFor("attempt"), + priorWriterGenerationRef: taskToPrRefFor("writer_generation"), + priorWorkRunRef: taskToPrRefFor("work_run"), + successorAttemptNonce: TaskToPrAttemptNonceSchema, + successorWriterGenerationRef: taskToPrRefFor("writer_generation"), + preservedStateRefs: z.array(TaskToPrRefSchema).min(1), + stopEvidenceRef: TaskToPrEvidenceRefSchema, + leaseRevocationEvidenceRef: TaskToPrEvidenceRefSchema + }) + .strict() + .superRefine((value, ctx) => { + requireFreshTaskToPrRef( + value.priorWriterGenerationRef, + value.successorWriterGenerationRef, + ctx, + ["successorWriterGenerationRef"], + "Recovery writer-generation rotation" + ); + requireDistinctTaskToPrEvidenceRefs( + value.stopEvidenceRef, + value.leaseRevocationEvidenceRef, + ctx, + ["leaseRevocationEvidenceRef"] + ); + }); +export type TaskToPrRecovery = z.infer; + +export const TaskToPrCancellationSchema = z + .object({ + ref: taskToPrRefFor("cancellation"), + cancelledAttemptRef: taskToPrRefFor("attempt"), + preservedStateRefs: z.array(TaskToPrRefSchema).min(1), + evidenceRefs: z.array(TaskToPrEvidenceRefSchema).min(1) + }) + .strict(); +export type TaskToPrCancellation = z.infer; + +export const TaskToPrCleanupEligibilitySchema = z + .object({ + ref: taskToPrRefFor("cleanup_eligibility"), + status: z.enum(["not_ready", "preserved", "blocked", "eligible"]), + targetWorktreeRef: taskToPrRefFor("worktree"), + eventCursorRef: taskToPrRefFor("replay_cursor"), + terminalDispositionRef: taskToPrRefFor("terminal_disposition"), + writerLeaseRef: taskToPrRefFor("writer_lease"), + leaseRevocationEvidenceRef: TaskToPrEvidenceRefSchema, + consumedEventEvidenceRef: TaskToPrEvidenceRefSchema, + evaluatedAt: TimestampSchema, + evidenceRefs: z.array(TaskToPrEvidenceRefSchema).min(1) + }) + .strict() + .superRefine((value, ctx) => { + if ( + value.leaseRevocationEvidenceRef.id === value.consumedEventEvidenceRef.id || + value.leaseRevocationEvidenceRef.digest === value.consumedEventEvidenceRef.digest + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Cleanup lease-revocation and consumed-event facts require distinct evidence identities and digests", + path: ["consumedEventEvidenceRef"] + }); + } + }); +export type TaskToPrCleanupEligibility = z.infer; + +export const TaskToPrCleanupOutcomeSchema = z + .object({ + ref: taskToPrRefFor("cleanup_outcome"), + eligibilityRef: taskToPrRefFor("cleanup_eligibility"), + targetWorktreeRef: taskToPrRefFor("worktree"), + status: z.enum(["preserved", "deleted", "failed", "skipped"]), + finishedAt: TimestampSchema, + evidenceRefs: z.array(TaskToPrEvidenceRefSchema).min(1) + }) + .strict(); +export type TaskToPrCleanupOutcome = z.infer; + +export const TaskToPrCleanupStateSchema = z + .object({ + eligibility: TaskToPrCleanupEligibilitySchema, + outcome: TaskToPrCleanupOutcomeSchema.optional() + }) + .strict() + .superRefine((value, ctx) => { + if ( + value.outcome && + !sameTaskToPrRef(value.outcome.eligibilityRef, value.eligibility.ref) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup outcomes must bind the exact eligibility decision", + path: ["outcome", "eligibilityRef"] + }); + } + if ( + value.outcome && + !sameTaskToPrRef(value.outcome.targetWorktreeRef, value.eligibility.targetWorktreeRef) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup eligibility and outcome must bind the same target worktree", + path: ["outcome", "targetWorktreeRef"] + }); + } + if (value.outcome?.status === "deleted" && value.eligibility.status !== "eligible") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Deletion requires an eligible cleanup decision", + path: ["outcome", "status"] + }); + } + }); +export type TaskToPrCleanupState = z.infer; + +export const TaskToPrRollbackSchema = z + .object({ + plan: z + .object({ + ref: taskToPrRefFor("rollback_plan"), + targetRef: z.union([taskToPrRefFor("commit"), taskToPrRefFor("branch")]), + createdAt: TimestampSchema + }) + .strict(), + outcome: z + .object({ + ref: taskToPrRefFor("rollback_outcome"), + planRef: taskToPrRefFor("rollback_plan"), + targetRef: z.union([taskToPrRefFor("commit"), taskToPrRefFor("branch")]), + status: z.enum(["not_run", "succeeded", "failed", "cancelled"]), + finishedAt: TimestampSchema, + evidenceRefs: z.array(TaskToPrEvidenceRefSchema).min(1) + }) + .strict() + .optional() + }) + .strict() + .superRefine((value, ctx) => { + if (value.outcome && !sameTaskToPrRef(value.outcome.planRef, value.plan.ref)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Rollback outcomes must bind the exact rollback plan", + path: ["outcome", "planRef"] + }); + } + if (value.outcome && !sameTaskToPrRef(value.outcome.targetRef, value.plan.targetRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Rollback outcomes must bind the exact rollback target", + path: ["outcome", "targetRef"] + }); + } + if (value.outcome && Date.parse(value.outcome.finishedAt) < Date.parse(value.plan.createdAt)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Rollback outcomes cannot finish before their plan was created", + path: ["outcome", "finishedAt"] + }); + } + }); +export type TaskToPrRollback = z.infer; + +export type TaskToPrProvenanceEntry = + | { + category: + | "work_run" + | "attempt" + | "admission" + | "worker_actor" + | "worker_assignment" + | "runtime" + | "writer_generation" + | "writer_lease" + | "writer_fence" + | "provider_profile" + | "provider_route" + | "replay_cursor" + | "repair_state" + | "latest_repair" + | "handoff" + | "recovery" + | "merge_guard" + | "cleanup_eligibility" + | "rollback_plan" + | "terminal_disposition"; + ref: TaskToPrRef; + } + | { + category: + | "equality_proof" + | "ci_proof" + | "review_proof" + | "review_record" + | "review_run" + | "provider_guard_receipt"; + ref: TaskToPrRef; + base: TaskToPrGitObjectId; + head: TaskToPrGitObjectId; + } + | { + category: "projection_id"; + projectionId: string; + } + | { + category: "attempt_nonce"; + nonce: string; + } + | { + category: "replay_prefix"; + sequence: number; + prefixDigest: string; + }; + +export const TaskToPrProvenanceEntrySchema: z.ZodType = z.discriminatedUnion("category", [ + z + .object({ + category: z.literal("projection_id"), + projectionId: TaskToPrProjectionIdSchema + }) + .strict(), + z + .object({ + category: z.literal("work_run"), + ref: taskToPrRefFor("work_run") + }) + .strict(), + z + .object({ + category: z.literal("attempt"), + ref: taskToPrRefFor("attempt") + }) + .strict(), + z + .object({ + category: z.literal("admission"), + ref: taskToPrRefFor("admission") + }) + .strict(), + z + .object({ + category: z.literal("worker_actor"), + ref: taskToPrRefFor("worker_actor") + }) + .strict(), + z + .object({ + category: z.literal("worker_assignment"), + ref: taskToPrRefFor("worker") + }) + .strict(), + z + .object({ + category: z.literal("attempt_nonce"), + nonce: TaskToPrAttemptNonceSchema + }) + .strict(), + z + .object({ + category: z.literal("runtime"), + ref: taskToPrRefFor("runtime") + }) + .strict(), + z + .object({ + category: z.literal("writer_generation"), + ref: taskToPrRefFor("writer_generation") + }) + .strict(), + z + .object({ + category: z.literal("writer_lease"), + ref: taskToPrRefFor("writer_lease") + }) + .strict(), + z + .object({ + category: z.literal("writer_fence"), + ref: taskToPrRefFor("writer_fence") + }) + .strict(), + z + .object({ + category: z.literal("provider_profile"), + ref: taskToPrRefFor("provider_profile") + }) + .strict(), + z + .object({ + category: z.literal("provider_route"), + ref: taskToPrRefFor("provider_route") + }) + .strict(), + z + .object({ + category: z.literal("replay_cursor"), + ref: taskToPrRefFor("replay_cursor") + }) + .strict(), + z + .object({ + category: z.literal("replay_prefix"), + sequence: z.number().int().safe().nonnegative(), + prefixDigest: LowerSha256DigestSchema + }) + .strict(), + z + .object({ + category: z.literal("repair_state"), + ref: taskToPrRefFor("repair_cycle") + }) + .strict(), + z + .object({ + category: z.literal("latest_repair"), + ref: taskToPrRefFor("repair_cycle") + }) + .strict(), + z + .object({ + category: z.literal("handoff"), + ref: taskToPrRefFor("handoff") + }) + .strict(), + z + .object({ + category: z.literal("recovery"), + ref: taskToPrRefFor("recovery") + }) + .strict(), + z + .object({ + category: z.literal("merge_guard"), + ref: taskToPrRefFor("merge_guard") + }) + .strict(), + z + .object({ + category: z.literal("cleanup_eligibility"), + ref: taskToPrRefFor("cleanup_eligibility") + }) + .strict(), + z + .object({ + category: z.literal("rollback_plan"), + ref: taskToPrRefFor("rollback_plan") + }) + .strict(), + z + .object({ + category: z.literal("terminal_disposition"), + ref: taskToPrRefFor("terminal_disposition") + }) + .strict(), + z + .object({ + category: z.literal("equality_proof"), + ref: taskToPrRefFor("proof_bundle"), + base: TaskToPrGitObjectIdSchema, + head: TaskToPrGitObjectIdSchema + }) + .strict(), + z + .object({ + category: z.literal("ci_proof"), + ref: taskToPrRefFor("proof_bundle"), + base: TaskToPrGitObjectIdSchema, + head: TaskToPrGitObjectIdSchema + }) + .strict(), + z + .object({ + category: z.literal("review_proof"), + ref: taskToPrRefFor("proof_bundle"), + base: TaskToPrGitObjectIdSchema, + head: TaskToPrGitObjectIdSchema + }) + .strict(), + z + .object({ + category: z.literal("review_record"), + ref: taskToPrRefFor("review"), + base: TaskToPrGitObjectIdSchema, + head: TaskToPrGitObjectIdSchema + }) + .strict(), + z + .object({ + category: z.literal("review_run"), + ref: taskToPrRefFor("review_run"), + base: TaskToPrGitObjectIdSchema, + head: TaskToPrGitObjectIdSchema + }) + .strict(), + z + .object({ + category: z.literal("provider_guard_receipt"), + ref: taskToPrRefFor("merge_guard_receipt"), + base: TaskToPrGitObjectIdSchema, + head: TaskToPrGitObjectIdSchema + }) + .strict() +]); + +type TaskToPrProvenanceProjectionView = { + id: string; + workRunRef: TaskToPrRef; + attempt: TaskToPrAttempt; + events: TaskToPrEventCursor; + repair: TaskToPrRepairState; + handoff?: TaskToPrHandoff | undefined; + recovery?: TaskToPrRecovery | undefined; + exactHead?: TaskToPrExactHeadBinding | undefined; + reviews: TaskToPrReviewBinding[]; + merge?: TaskToPrMergeState | undefined; + cleanup?: TaskToPrCleanupState | undefined; + rollback?: TaskToPrRollback | undefined; + terminalDispositionRef?: TaskToPrRef | undefined; +}; + +function taskToPrActiveProvenanceEntries( + projection: TaskToPrProvenanceProjectionView +): TaskToPrProvenanceEntry[] { + return [ + { + category: "projection_id" as const, + projectionId: projection.id + }, + { + category: "work_run" as const, + ref: projection.workRunRef + }, + { + category: "attempt" as const, + ref: projection.attempt.ref + }, + { + category: "admission" as const, + ref: projection.attempt.admissionRef + }, + { + category: "worker_actor" as const, + ref: projection.attempt.workerActorRef + }, + { + category: "worker_assignment" as const, + ref: projection.attempt.workerRef + }, + { + category: "attempt_nonce" as const, + nonce: projection.attempt.nonce + }, + { + category: "runtime" as const, + ref: projection.attempt.runtimeRef + }, + { + category: "writer_generation" as const, + ref: projection.attempt.writerGenerationRef + }, + { + category: "writer_lease" as const, + ref: projection.attempt.writerLeaseRef + }, + { + category: "writer_fence" as const, + ref: projection.attempt.writerFenceRef + }, + { + category: "provider_profile" as const, + ref: projection.attempt.providerProfileRef + }, + { + category: "provider_route" as const, + ref: projection.attempt.providerRouteRef + }, + { + category: "replay_cursor" as const, + ref: projection.events.replayCursorRef + }, + { + category: "replay_prefix" as const, + sequence: projection.events.sequence, + prefixDigest: projection.events.prefixDigest + }, + { + category: "repair_state" as const, + ref: projection.repair.ref + }, + ...(projection.repair.latestRepairRef + ? [ + { + category: "latest_repair" as const, + ref: projection.repair.latestRepairRef + } + ] + : []), + ...(projection.handoff + ? [{ category: "handoff" as const, ref: projection.handoff.ref }] + : []), + ...(projection.recovery + ? [{ category: "recovery" as const, ref: projection.recovery.ref }] + : []), + ...(projection.exactHead + ? [ + { + category: "equality_proof" as const, + ref: projection.exactHead.equalityProofRef, + base: projection.exactHead.expectedBase, + head: projection.exactHead.localHead + }, + ...projection.exactHead.ciProofBundleRefs.map((ref) => ({ + category: "ci_proof" as const, + ref, + base: projection.exactHead!.expectedBase, + head: projection.exactHead!.localHead + })) + ] + : []), + ...projection.reviews.flatMap((review) => [ + { + category: "review_proof" as const, + ref: review.proofBundleRef, + base: review.base, + head: review.head + }, + { + category: "review_record" as const, + ref: review.ref, + base: review.base, + head: review.head + }, + { + category: "review_run" as const, + ref: review.reviewRunRef, + base: review.base, + head: review.head + } + ]), + ...(projection.merge + ? [ + { + category: "merge_guard" as const, + ref: projection.merge.guard.ref + }, + { + category: "provider_guard_receipt" as const, + ref: projection.merge.guard.providerGuardReceiptRef, + base: projection.merge.guard.expectedBase, + head: projection.merge.guard.expectedHead + } + ] + : []), + ...(projection.cleanup + ? [ + { + category: "cleanup_eligibility" as const, + ref: projection.cleanup.eligibility.ref + } + ] + : []), + ...(projection.rollback + ? [ + { + category: "rollback_plan" as const, + ref: projection.rollback.plan.ref + } + ] + : []), + ...(projection.terminalDispositionRef + ? [ + { + category: "terminal_disposition" as const, + ref: projection.terminalDispositionRef + } + ] + : []) + ]; +} + +function sameTaskToPrProvenanceEntry( + left: TaskToPrProvenanceEntry, + right: TaskToPrProvenanceEntry +): boolean { + if (left.category !== right.category) { + return false; + } + if (left.category === "projection_id" && right.category === "projection_id") { + return left.projectionId === right.projectionId; + } + if (left.category === "attempt_nonce" && right.category === "attempt_nonce") { + return left.nonce === right.nonce; + } + if (left.category === "replay_prefix" && right.category === "replay_prefix") { + return left.sequence === right.sequence && left.prefixDigest === right.prefixDigest; + } + if (!("ref" in left) || !("ref" in right)) { + return false; + } + return ( + sameTaskToPrRef(left.ref, right.ref) && + (("head" in left && + "head" in right && + "base" in left && + "base" in right && + sameGitObjectId(left.base, right.base) && + sameGitObjectId(left.head, right.head)) || + (!("head" in left) && !("head" in right) && !("base" in left) && !("base" in right))) + ); +} + +export const TASK_TO_PR_V1_ADAPTER_EXTENSION_SCHEMA_PREFIX = + "hasna.task_to_pr_adapter_extension."; + +export const TaskToPrAdapterExtensionSchema = z + .object({ + mode: z.enum(["local", "cloud"]), + schema: SchemaIdSchema, + ref: taskToPrRefFor("adapter_extension"), + digest: LowerSha256DigestSchema + }) + .strict() + .superRefine((value, ctx) => { + if (!value.schema.startsWith(TASK_TO_PR_V1_ADAPTER_EXTENSION_SCHEMA_PREFIX)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Adapter extension schema ids must use the permanently reserved task-to-PR adapter-extension namespace", + path: ["schema"] + }); + } + }); +export type TaskToPrAdapterExtension = z.infer; + +export const TaskToPrProjectionStateSchema = z.enum([ + "admitted", + "running", + "handed_off", + "reviewing", + "repairing", + "merge_ready", + "merged", + "closed_unmerged", + "failed", + "blocked", + "cancelled", + "recovering", + "cleanup_complete", + "rolled_back" +]); +export type TaskToPrProjectionState = z.infer; + +const TaskToPrStatesWithoutReviewAuthority = new Set([ + "admitted", + "running", + "handed_off" +]); +const TaskToPrTerminalStates = new Set([ + "merged", + "closed_unmerged", + "failed", + "blocked", + "cancelled", + "cleanup_complete", + "rolled_back" +]); +const TASK_TO_PR_STATE_MERGE_MATRIX: Record< + TaskToPrProjectionState, + ReadonlySet +> = { + admitted: new Set(["absent", "denied:none", "revoked:none"]), + running: new Set(["absent", "denied:none", "revoked:none"]), + handed_off: new Set(["absent", "denied:none", "revoked:none"]), + reviewing: new Set(["absent", "denied:none", "revoked:none"]), + repairing: new Set(["absent", "denied:none", "revoked:none"]), + merge_ready: new Set(["eligible:none"]), + merged: new Set(["consumed:merged"]), + closed_unmerged: new Set([ + "consumed:closed_unmerged", + "consumed:refused", + "consumed:head_drift", + "consumed:base_drift" + ]), + failed: new Set(["absent", "revoked:none"]), + blocked: new Set(["absent", "revoked:none"]), + cancelled: new Set(["absent", "revoked:none"]), + recovering: new Set(["absent", "denied:none", "revoked:none"]), + cleanup_complete: new Set([ + "absent", + "revoked:none", + "consumed:merged", + "consumed:closed_unmerged", + "consumed:refused", + "consumed:head_drift", + "consumed:base_drift" + ]), + rolled_back: new Set(["consumed:merged"]) +}; + +export const TaskToPrProjectionSchema = z + .object({ + schema: z.literal(SCHEMA_IDS.taskToPrProjection), + id: TaskToPrProjectionIdSchema, + createdAt: TimestampSchema, + canonicalizationVersion: z.union([z.literal(1), z.literal(2)]), + identityDigest: LowerSha256DigestSchema, + frozenScopeDigest: LowerSha256DigestSchema, + state: TaskToPrProjectionStateSchema, + workRunRef: taskToPrRefFor("work_run"), + rootRequestRef: taskToPrRefFor("root_request"), + prGroupRef: taskToPrRefFor("pr_group"), + leafTaskRef: taskToPrRefFor("leaf_task"), + attempt: TaskToPrAttemptSchema, + repository: TaskToPrRepositoryBindingSchema, + events: TaskToPrEventCursorSchema, + openLoopsInvocationRef: taskToPrRefFor("openloops_invocation").optional(), + pullRequestRef: taskToPrRefFor("pull_request").optional(), + exactHead: TaskToPrExactHeadBindingSchema.optional(), + handoff: TaskToPrHandoffSchema.optional(), + reviews: z.array(TaskToPrReviewBindingSchema).default([]), + repair: TaskToPrRepairStateSchema, + merge: TaskToPrMergeStateSchema.optional(), + recovery: TaskToPrRecoverySchema.optional(), + cancellation: TaskToPrCancellationSchema.optional(), + cleanup: TaskToPrCleanupStateSchema.optional(), + rollback: TaskToPrRollbackSchema.optional(), + terminalDispositionRef: taskToPrRefFor("terminal_disposition").optional(), + provenanceLedger: z.array(TaskToPrProvenanceEntrySchema), + adapterExtensions: z.array(TaskToPrAdapterExtensionSchema).default([]), + evidenceRefs: z.array(TaskToPrEvidenceRefSchema).default([]) + }) + .strict() + .superRefine((value, ctx) => { + const derivedIdentityDigest = + value.canonicalizationVersion === 1 + ? deriveTaskToPrIdentityDigest({ + canonicalizationVersion: 1, + rootRequestRef: value.rootRequestRef, + prGroupRef: value.prGroupRef, + leafTaskRef: value.leafTaskRef, + repoRef: value.repository.repoRef, + baseHead: value.repository.baseHead, + frozenScopeDigest: value.frozenScopeDigest + }) + : deriveTaskToPrIdentityDigest({ + canonicalizationVersion: 2, + rootRequestRef: value.rootRequestRef, + prGroupRef: value.prGroupRef, + leafTaskRef: value.leafTaskRef, + repoRef: value.repository.repoRef, + worktreeRef: value.repository.worktreeRef, + branchRef: value.repository.branchRef, + baseHead: value.repository.baseHead, + frozenScopeDigest: value.frozenScopeDigest + }); + if (value.identityDigest !== derivedIdentityDigest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "identityDigest must equal the selected v1 compatibility or v2 branch/worktree-bound canonical identity digest", + path: ["identityDigest"] + }); + } + const provenanceIds = new Set(); + const provenanceDigests = new Set(); + const provenanceProjectionIds = new Set(); + const provenanceAttemptNonces = new Set(); + const provenanceReplayPrefixes = new Set(); + const provenanceReplaySequences = new Set(); + for (const [index, entry] of value.provenanceLedger.entries()) { + if ("ref" in entry) { + if (provenanceIds.has(entry.ref.id)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Provenance entries cannot reuse a canonical owner id across categories or generations", + path: ["provenanceLedger", index, "ref", "id"] + }); + } + provenanceIds.add(entry.ref.id); + if (provenanceDigests.has(entry.ref.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Provenance entries cannot reuse a canonical digest across categories or generations", + path: ["provenanceLedger", index, "ref", "digest"] + }); + } + provenanceDigests.add(entry.ref.digest); + continue; + } + if (entry.category === "projection_id") { + if (provenanceProjectionIds.has(entry.projectionId)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Projection identity provenance tombstones must be globally unique", + path: ["provenanceLedger", index, "projectionId"] + }); + } + provenanceProjectionIds.add(entry.projectionId); + continue; + } + if (entry.category === "attempt_nonce") { + if (provenanceAttemptNonces.has(entry.nonce)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Attempt nonce provenance tombstones must be globally unique", + path: ["provenanceLedger", index, "nonce"] + }); + } + provenanceAttemptNonces.add(entry.nonce); + continue; + } + if (provenanceReplayPrefixes.has(entry.prefixDigest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Replay prefix provenance tombstones must be globally unique", + path: ["provenanceLedger", index, "prefixDigest"] + }); + } + provenanceReplayPrefixes.add(entry.prefixDigest); + if (provenanceReplaySequences.has(entry.sequence)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Replay prefix provenance entries must bind globally unique replay sequences", + path: ["provenanceLedger", index, "sequence"] + }); + } + provenanceReplaySequences.add(entry.sequence); + } + for (const activeEntry of taskToPrActiveProvenanceEntries(value)) { + if (!value.provenanceLedger.some((entry) => sameTaskToPrProvenanceEntry(entry, activeEntry))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The active ${activeEntry.category} identity must be represented exactly in the monotonic provenance ledger`, + path: ["provenanceLedger"] + }); + } + } + const requiredCanonicalPreservationRefs = [ + value.rootRequestRef, + value.prGroupRef, + value.leafTaskRef, + value.repository.repoRef, + value.repository.worktreeRef, + value.repository.branchRef, + value.events.streamRef, + ...(value.pullRequestRef ? [value.pullRequestRef] : []) + ]; + const requirePreservedRefs = ( + preservedStateRefs: TaskToPrRef[], + requiredRefs: TaskToPrRef[], + path: (string | number)[], + label: string + ) => { + const requiredRoles = new Set(requiredRefs.map((requiredRef) => requiredRef.role)); + const seenRoles = new Set(); + for (const [index, preservedRef] of preservedStateRefs.entries()) { + if (!requiredRoles.has(preservedRef.role)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${label} cannot preserve an unrecognized ${preservedRef.role} role`, + path: [...path, index] + }); + } + if (seenRoles.has(preservedRef.role)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${label} must preserve exactly one canonical ref per role`, + path: [...path, index] + }); + } + seenRoles.add(preservedRef.role); + } + if (preservedStateRefs.length !== requiredRefs.length) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${label} preservation refs must exactly equal the required canonical role set`, + path + }); + } + for (const requiredRef of requiredRefs) { + if (!preservedStateRefs.some((preservedRef) => sameTaskToPrRef(preservedRef, requiredRef))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${label} must preserve ${requiredRef.role}`, + path + }); + } + } + }; + if (value.handoff && !sameTaskToPrRef(value.handoff.nextWriterGenerationRef, value.attempt.writerGenerationRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Handoff next generation must be the current attempt writer generation", + path: ["handoff", "nextWriterGenerationRef"] + }); + } + if (value.handoff && !sameTaskToPrRef(value.handoff.nextAttemptRef, value.attempt.ref)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Handoff next attempt must be the current attempt", + path: ["handoff", "nextAttemptRef"] + }); + } + if (value.handoff) { + requireFreshTaskToPrRef( + value.handoff.stoppedWorkRunRef, + value.workRunRef, + ctx, + ["handoff", "stoppedWorkRunRef"], + "Handoff WorkRun rotation" + ); + } + if (value.recovery) { + if (value.recovery.successorAttemptNonce !== value.attempt.nonce) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Recovery successor nonce must equal the current attempt nonce", + path: ["recovery", "successorAttemptNonce"] + }); + } + if (!sameTaskToPrRef(value.recovery.successorWriterGenerationRef, value.attempt.writerGenerationRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Recovery successor generation must equal the current writer generation", + path: ["recovery", "successorWriterGenerationRef"] + }); + } + requireFreshTaskToPrRef( + value.recovery.priorAttemptRef, + value.attempt.ref, + ctx, + ["recovery", "priorAttemptRef"], + "Recovery attempt rotation" + ); + requireFreshTaskToPrRef( + value.recovery.priorWorkRunRef, + value.workRunRef, + ctx, + ["recovery", "priorWorkRunRef"], + "Recovery WorkRun rotation" + ); + requirePreservedRefs( + value.recovery.preservedStateRefs, + [value.recovery.priorWorkRunRef, ...requiredCanonicalPreservationRefs], + ["recovery", "preservedStateRefs"], + "Recovery" + ); + } + if (value.cancellation && !sameTaskToPrRef(value.cancellation.cancelledAttemptRef, value.attempt.ref)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cancellation must bind the current attempt", + path: ["cancellation", "cancelledAttemptRef"] + }); + } + if (value.cancellation) { + requirePreservedRefs( + value.cancellation.preservedStateRefs, + [value.workRunRef, value.attempt.ref, ...requiredCanonicalPreservationRefs], + ["cancellation", "preservedStateRefs"], + "Cancellation" + ); + } + if (value.cancellation && value.recovery) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "A projection cannot be both the cancellation and recovery snapshot", + path: ["recovery"] + }); + } + if (value.handoff && value.recovery) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "A projection cannot be both the handoff and recovery snapshot", + path: ["recovery"] + }); + } + if (value.cleanup && !sameTaskToPrRef(value.cleanup.eligibility.eventCursorRef, value.events.replayCursorRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup eligibility must bind the current canonical replay cursor", + path: ["cleanup", "eligibility", "eventCursorRef"] + }); + } + if ( + value.cleanup && + (!value.terminalDispositionRef || + !sameTaskToPrRef( + value.cleanup.eligibility.terminalDispositionRef, + value.terminalDispositionRef + )) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup eligibility must bind the exact durable terminal owner fact", + path: ["cleanup", "eligibility", "terminalDispositionRef"] + }); + } + if ( + value.cleanup && + !sameTaskToPrRef(value.cleanup.eligibility.writerLeaseRef, value.attempt.writerLeaseRef) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup eligibility must bind the exact writer lease being revoked", + path: ["cleanup", "eligibility", "writerLeaseRef"] + }); + } + if ( + value.cleanup && + !sameTaskToPrRef(value.cleanup.eligibility.targetWorktreeRef, value.repository.worktreeRef) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup eligibility must bind the canonical worktree", + path: ["cleanup", "eligibility", "targetWorktreeRef"] + }); + } + if (value.pullRequestRef) { + if (value.exactHead && !sameTaskToPrRef(value.exactHead.pullRequestRef, value.pullRequestRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Exact-head proof must bind the canonical pull request ref", + path: ["exactHead", "pullRequestRef"] + }); + } + for (const [reviewIndex, review] of value.reviews.entries()) { + if (!sameTaskToPrRef(review.pullRequestRef, value.pullRequestRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Every review must bind the canonical pull request ref", + path: ["reviews", reviewIndex, "pullRequestRef"] + }); + } + } + if (value.merge && !sameTaskToPrRef(value.merge.guard.pullRequestRef, value.pullRequestRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guard must bind the canonical pull request ref", + path: ["merge", "guard", "pullRequestRef"] + }); + } + if (value.merge?.outcome && !sameTaskToPrRef(value.merge.outcome.pullRequestRef, value.pullRequestRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge outcome must bind the canonical pull request ref", + path: ["merge", "outcome", "pullRequestRef"] + }); + } + } else if (value.exactHead || value.reviews.length > 0 || value.merge) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review and merge state require a canonical pull request ref", + path: ["pullRequestRef"] + }); + } + if (value.exactHead && !sameGitObjectId(value.exactHead.localHead, value.repository.branchHead)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Exact local head must equal the canonical branch head", + path: ["exactHead", "localHead"] + }); + } + if (value.exactHead && !sameGitObjectId(value.exactHead.expectedBase, value.repository.baseHead)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Exact-head expected base must equal the canonical repository base", + path: ["exactHead", "expectedBase"] + }); + } + if (value.exactHead && !sameTaskToPrRef(value.exactHead.remoteBranchRef, value.repository.branchRef)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Exact-head remote branch ref must equal the canonical repository branch ref", + path: ["exactHead", "remoteBranchRef"] + }); + } + if (value.exactHead && Date.parse(value.exactHead.verifiedAt) < Date.parse(value.createdAt)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Exact-head verification cannot precede the projection timestamp", + path: ["exactHead", "verifiedAt"] + }); + } + if (value.reviews.length > 0 && !value.exactHead) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Reviews require local/remote/provider exact-head proof", + path: ["exactHead"] + }); + } + if (value.exactHead) { + const proofObligations: Array<{ ref: TaskToPrRef; path: (string | number)[] }> = [ + { ref: value.exactHead.equalityProofRef, path: ["exactHead", "equalityProofRef"] }, + ...value.exactHead.ciProofBundleRefs.map((ref, index) => ({ + ref, + path: ["exactHead", "ciProofBundleRefs", index] + })), + ...value.reviews.map((review, index) => ({ + ref: review.proofBundleRef, + path: ["reviews", index, "proofBundleRef"] + })) + ]; + const proofObligationKeys = new Set(); + const proofObligationDigests = new Set(); + for (const obligation of proofObligations) { + const key = taskToPrCanonicalRefKey(obligation.ref); + if (proofObligationKeys.has(key)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Exact-head equality, CI, and review proof obligations require globally unique canonical identities", + path: obligation.path + }); + } + proofObligationKeys.add(key); + if (proofObligationDigests.has(obligation.ref.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Exact-head equality, CI, and review proof obligations require globally unique canonical digests", + path: obligation.path + }); + } + proofObligationDigests.add(obligation.ref.digest); + } + } + const reviewKeys = new Set(); + const reviewDigests = new Set(); + const reviewerKeys = new Set(); + const reviewerDigests = new Set(); + const reviewRunKeys = new Set(); + const reviewRunDigests = new Set(); + const reviewProofKeys = new Set(); + const reviewProofDigests = new Set(); + for (const [reviewIndex, review] of value.reviews.entries()) { + if (!sameGitObjectId(review.base, value.repository.baseHead)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review base must equal the exact canonical pull-request base", + path: ["reviews", reviewIndex, "base"] + }); + } + if (!sameGitObjectId(review.head, value.repository.branchHead)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review head must equal the exact canonical branch head", + path: ["reviews", reviewIndex, "head"] + }); + } + for (const [key, seen, path] of [ + [review.ref.id, reviewKeys, "ref"], + [review.reviewerRef.id, reviewerKeys, "reviewerRef"], + [review.reviewRunRef.id, reviewRunKeys, "reviewRunRef"] + ] as const) { + if (seen.has(key)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review, reviewer, and review-run refs must each be unique", + path: ["reviews", reviewIndex, path] + }); + } + seen.add(key); + } + if (reviewDigests.has(review.ref.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review refs must resolve to distinct canonical record digests", + path: ["reviews", reviewIndex, "ref"] + }); + } + reviewDigests.add(review.ref.digest); + if (reviewerDigests.has(review.reviewerRef.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Reviewer refs must resolve to distinct canonical actor digests", + path: ["reviews", reviewIndex, "reviewerRef"] + }); + } + reviewerDigests.add(review.reviewerRef.digest); + if (reviewRunDigests.has(review.reviewRunRef.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review-run refs must resolve to distinct canonical run digests", + path: ["reviews", reviewIndex, "reviewRunRef"] + }); + } + reviewRunDigests.add(review.reviewRunRef.digest); + const reviewProofKey = taskToPrCanonicalRefKey(review.proofBundleRef); + if (reviewProofKeys.has(reviewProofKey)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review proof bundles must have unique canonical identities", + path: ["reviews", reviewIndex, "proofBundleRef"] + }); + } + reviewProofKeys.add(reviewProofKey); + if (reviewProofDigests.has(review.proofBundleRef.digest)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review proof bundles must have unique canonical digests", + path: ["reviews", reviewIndex, "proofBundleRef"] + }); + } + reviewProofDigests.add(review.proofBundleRef.digest); + if (review.reviewerRef.digest === value.attempt.workerRef.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Worker and reviewer identities must resolve to distinct canonical digests", + path: ["reviews", reviewIndex, "reviewerRef"] + }); + } + if (review.reviewRunRef.digest === value.attempt.runtimeRef.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Worker runtime and review run must resolve to distinct canonical digests", + path: ["reviews", reviewIndex, "reviewRunRef"] + }); + } + if ( + value.exactHead && + Date.parse(review.reviewedAt) < Date.parse(value.exactHead.verifiedAt) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Reviews cannot precede exact-head verification", + path: ["reviews", reviewIndex, "reviewedAt"] + }); + } + } + if (value.merge) { + if (!value.exactHead) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge state requires local/remote/provider exact-head proof", + path: ["exactHead"] + }); + } + if (!sameGitObjectId(value.merge.guard.expectedBase, value.repository.baseHead)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guard expected base must equal the exact canonical pull-request base", + path: ["merge", "guard", "expectedBase"] + }); + } + if (!sameGitObjectId(value.merge.guard.expectedHead, value.repository.branchHead)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guard expected head must equal the exact canonical branch head", + path: ["merge", "guard", "expectedHead"] + }); + } + if (value.exactHead && Date.parse(value.merge.guard.evaluatedAt) < Date.parse(value.exactHead.verifiedAt)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guards cannot precede exact-head verification", + path: ["merge", "guard", "evaluatedAt"] + }); + } + if (value.reviews.some((review) => Date.parse(value.merge!.guard.evaluatedAt) < Date.parse(review.reviewedAt))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge guards cannot precede their bound reviews", + path: ["merge", "guard", "evaluatedAt"] + }); + } + if (value.merge.guard.operatorRef.digest === value.attempt.workerRef.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Worker and merge operator identities must resolve to distinct canonical digests", + path: ["merge", "guard", "operatorRef"] + }); + } + if (value.merge.guard.operatorRunRef.digest === value.attempt.runtimeRef.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Worker runtime and merge-operator run must resolve to distinct canonical digests", + path: ["merge", "guard", "operatorRunRef"] + }); + } + for (const [reviewIndex, review] of value.reviews.entries()) { + if (value.merge.guard.operatorRef.digest === review.reviewerRef.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Reviewer and merge operator identities must resolve to distinct canonical digests", + path: ["reviews", reviewIndex, "reviewerRef"] + }); + } + if (value.merge.guard.operatorRunRef.digest === review.reviewRunRef.digest) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Review and merge-operator runs must resolve to distinct canonical digests", + path: ["reviews", reviewIndex, "reviewRunRef"] + }); + } + } + if ( + value.merge.guard.decision === "eligible" || + value.merge.guard.decision === "consumed" + ) { + if (value.reviews.length === 0 || value.reviews.some((review) => review.verdict !== "approved")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Eligible merge guards require at least one review and all reviews approved", + path: ["merge", "guard", "decision"] + }); + } + if ( + value.merge.guard.reviewRefs.length !== value.reviews.length || + value.merge.guard.reviewRefs.some( + (reviewRef) => !value.reviews.some((review) => sameTaskToPrRef(reviewRef, review.ref)) + ) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "Eligible merge guard review refs must exactly equal the projected approved review refs as a canonical set", + path: ["merge", "guard", "reviewRefs"] + }); + } + for (const review of value.reviews) { + if (!value.merge.guard.proofBundleRefs.some((proofRef) => sameTaskToPrRef(proofRef, review.proofBundleRef))) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Eligible merge guards must bind every exact review proof bundle", + path: ["merge", "guard", "proofBundleRefs"] + }); + } + } + if ( + value.exactHead && + !value.merge.guard.proofBundleRefs.some((proofRef) => sameTaskToPrRef(proofRef, value.exactHead!.equalityProofRef)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Eligible merge guards must bind the exact-head equality proof", + path: ["merge", "guard", "proofBundleRefs"] + }); + } + if ( + value.exactHead && + value.exactHead.ciProofBundleRefs.some( + (ciProofRef) => !value.merge!.guard.proofBundleRefs.some((proofRef) => sameTaskToPrRef(proofRef, ciProofRef)) + ) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Eligible merge guards must bind every exact-head CI proof", + path: ["merge", "guard", "proofBundleRefs"] + }); + } + } + } + if (value.merge?.outcome) { + if (!sameTaskToPrRef(value.merge.outcome.guardRef, value.merge.guard.ref)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge outcome must bind the exact immutable merge guard", + path: ["merge", "outcome", "guardRef"] + }); + } + if (!sameGitObjectId(value.merge.outcome.expectedHead, value.merge.guard.expectedHead)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge outcome expected head must equal the guarded expected head", + path: ["merge", "outcome", "expectedHead"] + }); + } + if (!sameGitObjectId(value.merge.outcome.expectedBase, value.merge.guard.expectedBase)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge outcome expected base must equal the guarded expected base", + path: ["merge", "outcome", "expectedBase"] + }); + } + if (value.merge.guard.decision !== "consumed") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Every merge outcome requires an explicitly consumed merge guard", + path: ["merge", "guard", "decision"] + }); + } + if (Date.parse(value.merge.outcome.finishedAt) < Date.parse(value.merge.guard.evaluatedAt)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge outcomes cannot precede guard evaluation", + path: ["merge", "outcome", "finishedAt"] + }); + } + } + if (value.cleanup) { + const cleanupFloor = value.merge?.outcome?.finishedAt ?? value.createdAt; + if (Date.parse(value.cleanup.eligibility.evaluatedAt) < Date.parse(cleanupFloor)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup eligibility cannot precede the terminal merge outcome or projection", + path: ["cleanup", "eligibility", "evaluatedAt"] + }); + } + if ( + value.cleanup.outcome && + Date.parse(value.cleanup.outcome.finishedAt) < Date.parse(value.cleanup.eligibility.evaluatedAt) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup outcomes cannot precede cleanup eligibility", + path: ["cleanup", "outcome", "finishedAt"] + }); + } + } + if ( + value.rollback?.outcome && + value.merge?.outcome && + Date.parse(value.rollback.outcome.finishedAt) < Date.parse(value.merge.outcome.finishedAt) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Rollback outcomes cannot precede the merge outcome they remediate", + path: ["rollback", "outcome", "finishedAt"] + }); + } + if (value.rollback) { + const rollbackFloor = value.merge?.outcome?.finishedAt ?? value.createdAt; + if (Date.parse(value.rollback.plan.createdAt) < Date.parse(rollbackFloor)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Rollback plans cannot precede the terminal merge outcome or projection", + path: ["rollback", "plan", "createdAt"] + }); + } + } + if (value.state === "handed_off" && !value.handoff) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Handed-off projections require a handoff ref", path: ["handoff"] }); + } + if (TaskToPrStatesWithoutReviewAuthority.has(value.state) && value.reviews.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value.state} projections cannot carry review bindings before review authority is active`, + path: ["reviews"] + }); + } + if ( + (TaskToPrStatesWithoutReviewAuthority.has(value.state) || value.state === "recovering") && + (value.merge?.guard.reviewRefs.length ?? 0) > 0 + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value.state} projections cannot hide review bindings in a merge guard before review authority is active`, + path: ["merge", "guard", "reviewRefs"] + }); + } + const mergeMatrixKey = value.merge + ? `${value.merge.guard.decision}:${value.merge.outcome?.status ?? "none"}` + : "absent"; + if (!TASK_TO_PR_STATE_MERGE_MATRIX[value.state].has(mergeMatrixKey)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `State ${value.state} is incompatible with merge authority ${mergeMatrixKey}`, + path: ["merge"] + }); + } + if (TaskToPrTerminalStates.has(value.state) && !value.terminalDispositionRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value.state} projections require a durable Todos terminal-disposition owner ref`, + path: ["terminalDispositionRef"] + }); + } + if (!TaskToPrTerminalStates.has(value.state) && value.terminalDispositionRef) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value.state} projections cannot carry a terminal-disposition owner ref`, + path: ["terminalDispositionRef"] + }); + } + if (value.state === "reviewing" && value.reviews.length === 0) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Reviewing projections require review refs", path: ["reviews"] }); + } + if (value.state === "cancelled" && !value.cancellation) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Cancelled projections require preservation state", path: ["cancellation"] }); + } + if (value.cancellation && value.merge?.outcome) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cancellation cannot coexist with a terminal merge outcome", + path: ["cancellation"] + }); + } + if (value.state === "recovering" && !value.recovery) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Recovering projections require recovery state", path: ["recovery"] }); + } + if (value.state === "repairing" && value.repair.cycle === 0) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Repairing projections require a non-zero repair cycle", path: ["repair", "cycle"] }); + } + if ( + value.merge && + (value.merge.guard.decision === "eligible" || value.merge.guard.decision === "consumed") && + !sameTaskToPrRef(value.attempt.admissionWriterGenerationRef, value.attempt.writerGenerationRef) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Merge eligibility requires admission from the current writer generation", + path: ["attempt", "admissionWriterGenerationRef"] + }); + } + if (value.state === "merge_ready" && value.merge?.guard.decision !== "eligible") { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Merge-ready projections require an eligible guard", path: ["merge"] }); + } + if (value.state === "merged" && value.merge?.outcome?.status !== "merged") { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Merged projections require a merged immutable outcome", path: ["merge"] }); + } + if ( + value.state === "closed_unmerged" && + !value.merge?.outcome?.status.match(/^(closed_unmerged|refused|head_drift|base_drift)$/) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Closed-unmerged projections require a non-merged terminal outcome", + path: ["merge"] + }); + } + if ( + value.state === "cleanup_complete" && + (!value.cleanup?.outcome || !["deleted", "preserved", "skipped"].includes(value.cleanup.outcome.status)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Cleanup-complete projections require an immutable cleanup outcome", + path: ["cleanup"] + }); + } + if (value.state === "rolled_back" && value.rollback?.outcome?.status !== "succeeded") { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Rolled-back projections require a successful rollback outcome", + path: ["rollback"] + }); + } + if ((value.state === "failed" || value.state === "blocked") && value.evidenceRefs.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Failed and blocked projections require redacted evidence refs", + path: ["evidenceRefs"] + }); + } + if ( + ["admitted", "running", "handed_off", "reviewing", "repairing", "merge_ready", "recovering"].includes(value.state) && + (value.merge?.outcome || value.cancellation || value.cleanup?.outcome || value.rollback?.outcome) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Non-terminal projections cannot carry terminal owner outcomes", + path: ["state"] + }); + } + const extensionKeys = new Set(); + for (const [index, extension] of value.adapterExtensions.entries()) { + const key = `${extension.mode}:${extension.schema}`; + if (extensionKeys.has(key)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Adapter extensions must be unique per local/cloud mode and schema", + path: ["adapterExtensions", index] + }); + } + extensionKeys.add(key); + } + }); +export type TaskToPrProjection = z.infer; + +export interface TaskToPrTransitionIssue { + path: string; + message: string; +} + +export type TaskToPrTransitionResult = + | { success: true; issues: [] } + | { success: false; issues: TaskToPrTransitionIssue[] }; + +type TaskToPrHeadBoundEvidenceCategory = + | "equality_proof" + | "ci_proof" + | "review_proof" + | "review_record" + | "review_run"; + +interface TaskToPrHeadBoundEvidenceBinding { + category: TaskToPrHeadBoundEvidenceCategory; + ref: TaskToPrRef; + path: string; +} + +function taskToPrHeadBoundEvidenceBindings( + projection: TaskToPrProjection +): TaskToPrHeadBoundEvidenceBinding[] { + return [ + ...(projection.exactHead + ? [ + { + category: "equality_proof" as const, + ref: projection.exactHead.equalityProofRef, + path: "exactHead.equalityProofRef" + }, + ...projection.exactHead.ciProofBundleRefs.map((ref, index) => ({ + category: "ci_proof" as const, + ref, + path: `exactHead.ciProofBundleRefs.${index}` + })) + ] + : []), + ...projection.reviews.flatMap((review, index) => [ + { + category: "review_proof" as const, + ref: review.proofBundleRef, + path: `reviews.${index}.proofBundleRef` + }, + { + category: "review_record" as const, + ref: review.ref, + path: `reviews.${index}.ref` + }, + { + category: "review_run" as const, + ref: review.reviewRunRef, + path: `reviews.${index}.reviewRunRef` + } + ]) + ]; +} + +const TASK_TO_PR_CHANGED_HEAD_EVIDENCE_IDENTITY_MESSAGE = + "A changed branch head requires every head-bound evidence ref to use a fresh canonical identity"; +const TASK_TO_PR_CHANGED_HEAD_EVIDENCE_DIGEST_MESSAGE = + "A changed branch head requires every head-bound evidence ref to use a fresh digest"; + +const TASK_TO_PR_LEGAL_STATE_TRANSITIONS: Record = { + admitted: ["admitted", "running", "failed", "blocked", "cancelled", "recovering"], + running: [ + "running", + "handed_off", + "reviewing", + "repairing", + "merge_ready", + "failed", + "blocked", + "cancelled", + "recovering" + ], + handed_off: ["handed_off", "running", "reviewing", "repairing", "failed", "blocked", "cancelled", "recovering"], + reviewing: ["reviewing", "repairing", "merge_ready", "failed", "blocked", "cancelled", "recovering", "closed_unmerged"], + repairing: ["repairing", "running", "reviewing", "merge_ready", "failed", "blocked", "cancelled", "recovering", "closed_unmerged"], + merge_ready: ["merge_ready", "repairing", "merged", "closed_unmerged", "failed", "blocked", "cancelled"], + merged: ["merged", "cleanup_complete", "rolled_back"], + closed_unmerged: ["closed_unmerged", "cleanup_complete"], + failed: ["failed", "cleanup_complete", "rolled_back"], + blocked: ["blocked", "cancelled", "cleanup_complete"], + cancelled: ["cancelled", "cleanup_complete"], + recovering: ["recovering", "running", "handed_off", "failed", "blocked", "cancelled"], + cleanup_complete: ["cleanup_complete"], + rolled_back: ["rolled_back"] +}; + +function taskToPrParseIssues( + prefix: "previous" | "current" | "local" | "cloud", + issues: z.ZodIssue[] +): TaskToPrTransitionIssue[] { + return issues.map((issue) => ({ + path: [prefix, ...issue.path].join("."), + message: issue.message + })); +} + +export function validateTaskToPrProjectionTransition( + previousInput: unknown, + currentInput: unknown +): TaskToPrTransitionResult { + const issues: TaskToPrTransitionIssue[] = []; + const addIssue = (path: string, message: string) => issues.push({ path, message }); + const parsedPrevious = TaskToPrProjectionSchema.safeParse(previousInput); + const parsedCurrent = TaskToPrProjectionSchema.safeParse(currentInput); + if (!parsedPrevious.success) { + issues.push(...taskToPrParseIssues("previous", parsedPrevious.error.issues)); + } + if (!parsedCurrent.success) { + issues.push(...taskToPrParseIssues("current", parsedCurrent.error.issues)); + } + if (!parsedPrevious.success || !parsedCurrent.success) { + return { success: false, issues }; + } + const previous = parsedPrevious.data; + const current = parsedCurrent.data; + const previousActiveProvenance = taskToPrActiveProvenanceEntries(previous); + const currentActiveProvenance = taskToPrActiveProvenanceEntries(current); + if ( + current.provenanceLedger.length < previous.provenanceLedger.length || + previous.provenanceLedger.some( + (entry, index) => + !current.provenanceLedger[index] || + !sameTaskToPrProvenanceEntry(entry, current.provenanceLedger[index]!) + ) + ) { + addIssue( + "provenanceLedger", + "The provenance ledger is append-only and must retain the previous ledger as an exact immutable prefix" + ); + } + const appendedProvenance = current.provenanceLedger.slice(previous.provenanceLedger.length); + for (const [index, entry] of appendedProvenance.entries()) { + if (!currentActiveProvenance.some((activeEntry) => sameTaskToPrProvenanceEntry(activeEntry, entry))) { + addIssue( + `provenanceLedger.${previous.provenanceLedger.length + index}`, + "A newly appended provenance entry must represent an active identity in the current snapshot" + ); + } + } + for (const activeEntry of currentActiveProvenance) { + const existedBefore = previous.provenanceLedger.some((entry) => + sameTaskToPrProvenanceEntry(entry, activeEntry) + ); + const wasActiveBefore = previousActiveProvenance.some((entry) => + sameTaskToPrProvenanceEntry(entry, activeEntry) + ); + if (existedBefore && !wasActiveBefore) { + addIssue( + "provenanceLedger", + `The ${activeEntry.category} identity cannot reactivate after becoming inactive` + ); + } + } + const validateOwnerRecordTransition = ( + path: string, + previousRecord: { ref: TaskToPrRef } | undefined, + currentRecord: { ref: TaskToPrRef } | undefined + ): void => { + if ( + !previousRecord || + !currentRecord || + JSON.stringify(previousRecord) === JSON.stringify(currentRecord) + ) { + return; + } + if (sameTaskToPrCanonicalRefId(previousRecord.ref, currentRecord.ref)) { + addIssue( + path, + "An owner record must remain exactly immutable while its canonical ref identity is unchanged" + ); + return; + } + if (previousRecord.ref.digest === currentRecord.ref.digest) { + addIssue(path, "A rotated owner record requires both a fresh canonical ref identity and a fresh digest"); + } + }; + + if (previous.id !== current.id) { + addIssue("id", "The canonical top-level projection id is immutable across legal transitions"); + } + if (Date.parse(current.createdAt) < Date.parse(previous.createdAt)) { + addIssue("createdAt", "Projection timestamps cannot move backwards"); + } + const { + id: _previousId, + createdAt: _previousCreatedAt, + events: _previousEvents, + provenanceLedger: _previousProvenanceLedger, + ...previousSemanticState + } = previous; + const { + id: _currentId, + createdAt: _currentCreatedAt, + events: _currentEvents, + provenanceLedger: _currentProvenanceLedger, + ...currentSemanticState + } = current; + if ( + JSON.stringify(previousSemanticState) !== JSON.stringify(currentSemanticState) && + current.events.sequence <= previous.events.sequence + ) { + addIssue("events.sequence", "Semantic lifecycle drift requires replay sequence and cursor advancement"); + } + for (const [path, left, right] of [ + ["identityDigest", previous.identityDigest, current.identityDigest], + ["frozenScopeDigest", previous.frozenScopeDigest, current.frozenScopeDigest], + ["rootRequestRef", JSON.stringify(previous.rootRequestRef), JSON.stringify(current.rootRequestRef)], + ["prGroupRef", JSON.stringify(previous.prGroupRef), JSON.stringify(current.prGroupRef)], + ["leafTaskRef", JSON.stringify(previous.leafTaskRef), JSON.stringify(current.leafTaskRef)], + ["repository.repoRef", JSON.stringify(previous.repository.repoRef), JSON.stringify(current.repository.repoRef)], + ["repository.worktreeRef", JSON.stringify(previous.repository.worktreeRef), JSON.stringify(current.repository.worktreeRef)], + ["repository.branchRef", JSON.stringify(previous.repository.branchRef), JSON.stringify(current.repository.branchRef)], + ["repository.baseHead", JSON.stringify(previous.repository.baseHead), JSON.stringify(current.repository.baseHead)], + ["events.streamRef", JSON.stringify(previous.events.streamRef), JSON.stringify(current.events.streamRef)] + ] as const) { + if (left !== right) { + addIssue(path, "Canonical task-to-PR identity cannot change between projections"); + } + } + + if ( + previous.pullRequestRef && + (!current.pullRequestRef || !sameTaskToPrRef(previous.pullRequestRef, current.pullRequestRef)) + ) { + addIssue("pullRequestRef", "An established canonical pull-request identity cannot change or disappear"); + } + + if (sameGitObjectId(previous.repository.branchHead, current.repository.branchHead)) { + if ( + previous.exactHead && + JSON.stringify(current.exactHead) !== JSON.stringify(previous.exactHead) + ) { + addIssue( + "exactHead", + "An established same-head exact-head fact is immutable and cannot change or disappear" + ); + } + if ( + current.reviews.length < previous.reviews.length || + previous.reviews.some( + (previousReview, index) => + JSON.stringify(current.reviews[index]) !== JSON.stringify(previousReview) + ) + ) { + addIssue( + "reviews", + "Same-head review history is an exact immutable prefix; existing review bindings cannot move, change, or disappear" + ); + } + } else { + const previousHeadBoundEvidence = taskToPrHeadBoundEvidenceBindings(previous); + const currentHeadBoundEvidence = taskToPrHeadBoundEvidenceBindings(current); + const previousHeadBoundEvidenceKeys = new Set( + previousHeadBoundEvidence.map(({ ref }) => taskToPrCanonicalRefKey(ref)) + ); + const previousHeadBoundEvidenceDigests = new Set( + previousHeadBoundEvidence.map(({ ref }) => ref.digest) + ); + for (const { ref, path } of currentHeadBoundEvidence) { + if (previousHeadBoundEvidenceKeys.has(taskToPrCanonicalRefKey(ref))) { + addIssue(path, TASK_TO_PR_CHANGED_HEAD_EVIDENCE_IDENTITY_MESSAGE); + } + if (previousHeadBoundEvidenceDigests.has(ref.digest)) { + addIssue(path, TASK_TO_PR_CHANGED_HEAD_EVIDENCE_DIGEST_MESSAGE); + } + } + } + if (current.state === "recovering" && (current.exactHead || current.reviews.length > 0)) { + if ( + !sameGitObjectId(previous.repository.branchHead, current.repository.branchHead) || + JSON.stringify(previous.exactHead) !== JSON.stringify(current.exactHead) || + JSON.stringify(previous.reviews) !== JSON.stringify(current.reviews) + ) { + addIssue( + "recovery", + "Recovery may retain exact-head and review facts only unchanged from the immediately prior same-head snapshot" + ); + } + } + + if (current.events.sequence < previous.events.sequence) { + addIssue("events.sequence", "Replay sequence cannot decrease"); + } else if (current.events.sequence === previous.events.sequence) { + if ( + current.events.prefixDigest !== previous.events.prefixDigest || + !sameTaskToPrRef(current.events.replayCursorRef, previous.events.replayCursorRef) + ) { + addIssue("events", "An unchanged replay sequence must retain the same cursor and prefix digest"); + } + } else { + if (sameTaskToPrCanonicalRefId(current.events.replayCursorRef, previous.events.replayCursorRef)) { + addIssue("events.replayCursorRef", "An advanced replay sequence requires a fresh canonical replay cursor ref"); + } + if (current.events.replayCursorRef.digest === previous.events.replayCursorRef.digest) { + addIssue("events.replayCursorRef", "An advanced replay sequence requires a fresh replay cursor digest"); + } + if (current.events.prefixDigest === previous.events.prefixDigest) { + addIssue("events.prefixDigest", "An advanced replay sequence requires a fresh prefix digest"); + } + } + + if (current.repair.cycle < previous.repair.cycle || current.repair.cycle > previous.repair.cycle + 1) { + addIssue("repair.cycle", "Repair cycles are cumulative, monotonic, and append at most one cycle per transition"); + } + if (current.repair.cycle === previous.repair.cycle + 1) { + const previousRepairRefs = [ + previous.repair.ref, + ...(previous.repair.latestRepairRef ? [previous.repair.latestRepairRef] : []) + ]; + for (const previousRepairRef of previousRepairRefs) { + if (sameTaskToPrCanonicalRefId(current.repair.ref, previousRepairRef)) { + addIssue("repair.ref", "An advanced repair cycle requires a repair-state ref fresh from both prior repair slots"); + } + if (current.repair.ref.digest === previousRepairRef.digest) { + addIssue("repair.ref", "An advanced repair cycle requires a repair-state digest fresh from both prior repair slots"); + } + } + if (!current.repair.latestRepairRef) { + addIssue("repair.latestRepairRef", "An advanced repair cycle requires a fresh latest-repair ref"); + } else { + for (const previousRepairRef of previousRepairRefs) { + if (sameTaskToPrCanonicalRefId(current.repair.latestRepairRef, previousRepairRef)) { + addIssue( + "repair.latestRepairRef", + "An advanced repair cycle requires a latest-repair ref fresh from both prior repair slots" + ); + } + if (current.repair.latestRepairRef.digest === previousRepairRef.digest) { + addIssue( + "repair.latestRepairRef", + "An advanced repair cycle requires a latest-repair digest fresh from both prior repair slots" + ); + } + } + } + } else if ( + current.repair.cycle === previous.repair.cycle && + JSON.stringify(current.repair) !== JSON.stringify(previous.repair) + ) { + addIssue("repair", "An unchanged repair cycle must retain the same immutable repair refs"); + } + const enteringRepair = previous.state !== "repairing" && current.state === "repairing"; + if (enteringRepair && current.repair.cycle !== previous.repair.cycle + 1) { + addIssue("repair.cycle", "Every entry into repairing must consume exactly one repair cycle"); + } + if (enteringRepair && previous.repair.exhausted) { + addIssue("repair.exhausted", "An exhausted repair budget cannot enter repairing"); + } + if ( + TaskToPrTerminalStates.has(previous.state) && + JSON.stringify(current.repair) !== JSON.stringify(previous.repair) + ) { + addIssue("repair", "Repair state is frozen after terminal disposition"); + } + + const attemptChanged = !sameTaskToPrCanonicalRefId(previous.attempt.ref, current.attempt.ref); + const nonceChanged = previous.attempt.nonce !== current.attempt.nonce; + const generationChanged = !sameTaskToPrCanonicalRefId( + previous.attempt.writerGenerationRef, + current.attempt.writerGenerationRef + ); + const workRunChanged = !sameTaskToPrCanonicalRefId(previous.workRunRef, current.workRunRef); + if (new Set([attemptChanged, nonceChanged, generationChanged, workRunChanged]).size !== 1) { + addIssue( + "attempt", + "Attempt ref, nonce, writer generation, and WorkRun must either all remain stable or all advance together" + ); + } + if (attemptChanged && (!current.recovery && !current.handoff)) { + addIssue("attempt", "A fresh attempt requires an explicit recovery or handoff transition ref"); + } + if (attemptChanged && current.recovery && current.handoff) { + addIssue("attempt", "A fresh attempt must use exactly one recovery or handoff transition"); + } + if (!attemptChanged && (current.recovery || current.handoff) && !previous.recovery && !previous.handoff) { + addIssue("attempt", "Recovery and handoff transitions require a fresh attempt"); + } + if (!attemptChanged && JSON.stringify(previous.handoff) !== JSON.stringify(current.handoff)) { + addIssue( + "handoff", + "An unchanged attempt must retain its exact immutable handoff provenance" + ); + } + if (!attemptChanged && JSON.stringify(previous.recovery) !== JSON.stringify(current.recovery)) { + addIssue( + "recovery", + "An unchanged attempt must retain its exact immutable recovery provenance" + ); + } + if (!attemptChanged && JSON.stringify(previous.attempt) !== JSON.stringify(current.attempt)) { + addIssue("attempt", "An unchanged attempt identity cannot mutate attempt-scoped owner refs"); + } + if ( + !generationChanged && + !sameTaskToPrRef(previous.attempt.writerGenerationRef, current.attempt.writerGenerationRef) + ) { + addIssue("attempt.writerGenerationRef", "An unchanged writer-generation identity cannot mutate its digest"); + } + if (!workRunChanged && !sameTaskToPrRef(previous.workRunRef, current.workRunRef)) { + addIssue("workRunRef", "An unchanged WorkRun identity cannot mutate its digest"); + } + + if (attemptChanged) { + if (current.attempt.ref.digest === previous.attempt.ref.digest) { + addIssue("attempt.ref", "A fresh attempt requires a fresh attempt digest"); + } + if (current.attempt.writerGenerationRef.digest === previous.attempt.writerGenerationRef.digest) { + addIssue("attempt.writerGenerationRef", "A fresh attempt requires a fresh writer-generation digest"); + } + if (current.workRunRef.digest === previous.workRunRef.digest) { + addIssue("workRunRef", "A fresh attempt requires a fresh WorkRun digest"); + } + for (const field of [ + "admissionRef", + "workerRef", + "runtimeRef", + "writerLeaseRef", + "writerFenceRef", + "providerProfileRef", + "providerRouteRef" + ] as const) { + if ( + sameTaskToPrCanonicalRefId(previous.attempt[field], current.attempt[field]) || + previous.attempt[field].digest === current.attempt[field].digest + ) { + addIssue(`attempt.${field}`, `A fresh attempt requires a fresh ${field}`); + } + } + if (current.recovery) { + if (!sameTaskToPrRef(current.recovery.priorAttemptRef, previous.attempt.ref)) { + addIssue("recovery.priorAttemptRef", "Recovery must bind the immediately prior attempt"); + } + if (!sameTaskToPrRef(current.recovery.priorWriterGenerationRef, previous.attempt.writerGenerationRef)) { + addIssue("recovery.priorWriterGenerationRef", "Recovery must bind the immediately prior writer generation"); + } + if (!sameTaskToPrRef(current.recovery.priorWorkRunRef, previous.workRunRef)) { + addIssue("recovery.priorWorkRunRef", "Recovery must bind the immediately prior WorkRun"); + } + } + if (current.handoff) { + if (!sameTaskToPrRef(current.handoff.previousAttemptRef, previous.attempt.ref)) { + addIssue("handoff.previousAttemptRef", "Handoff must bind the immediately prior attempt"); + } + if (!sameTaskToPrRef(current.handoff.nextAttemptRef, current.attempt.ref)) { + addIssue("handoff.nextAttemptRef", "Handoff must bind the successor attempt"); + } + if (!sameTaskToPrRef(current.handoff.previousWriterGenerationRef, previous.attempt.writerGenerationRef)) { + addIssue("handoff.previousWriterGenerationRef", "Handoff must bind the immediately prior writer generation"); + } + if (!sameTaskToPrRef(current.handoff.stoppedWorkRunRef, previous.workRunRef)) { + addIssue("handoff.stoppedWorkRunRef", "Handoff must bind the immediately prior WorkRun"); + } + } + } + + if (!TASK_TO_PR_LEGAL_STATE_TRANSITIONS[previous.state].includes(current.state)) { + addIssue("state", `Illegal task-to-PR lifecycle transition from ${previous.state} to ${current.state}`); + } + + if (previous.merge?.guard.decision === "eligible" && current.state !== "merge_ready") { + if (!current.merge) { + addIssue( + "merge", + "Leaving merge_ready requires an explicit revoked or consumed successor for the eligible guard" + ); + } else { + const expectedDecision = current.merge.outcome ? "consumed" : "revoked"; + if (current.merge.guard.decision !== expectedDecision) { + addIssue( + "merge.guard.decision", + `Leaving merge_ready requires the eligible guard to become ${expectedDecision}` + ); + } + if (!sameTaskToPrMergeGuardLineageFacts(previous.merge.guard, current.merge.guard)) { + addIssue( + "merge.guard", + "A revoked or consumed successor guard must preserve the eligible guard's exact authority facts" + ); + } + if (Date.parse(current.merge.guard.evaluatedAt) < Date.parse(previous.merge.guard.evaluatedAt)) { + addIssue( + "merge.guard.evaluatedAt", + "A revoked or consumed successor guard cannot precede the eligible guard" + ); + } + } + } + + validateOwnerRecordTransition("handoff", previous.handoff, current.handoff); + validateOwnerRecordTransition("recovery", previous.recovery, current.recovery); + validateOwnerRecordTransition("merge.guard", previous.merge?.guard, current.merge?.guard); + if ( + previous.merge && + current.merge && + (!sameGitObjectId(previous.merge.guard.expectedBase, current.merge.guard.expectedBase) || + !sameGitObjectId(previous.merge.guard.expectedHead, current.merge.guard.expectedHead)) + ) { + if ( + sameTaskToPrCanonicalRefId( + previous.merge.guard.providerGuardReceiptRef, + current.merge.guard.providerGuardReceiptRef + ) + ) { + addIssue( + "merge.guard.providerGuardReceiptRef", + "A changed guarded base or head requires a fresh provider guard receipt identity" + ); + } + if ( + previous.merge.guard.providerGuardReceiptRef.digest === + current.merge.guard.providerGuardReceiptRef.digest + ) { + addIssue( + "merge.guard.providerGuardReceiptRef", + "A changed guarded base or head requires a fresh provider guard receipt digest" + ); + } + } + validateOwnerRecordTransition( + "cleanup.eligibility", + previous.cleanup?.eligibility, + current.cleanup?.eligibility + ); + validateOwnerRecordTransition("rollback.plan", previous.rollback?.plan, current.rollback?.plan); + if ( + previous.terminalDispositionRef && + (!current.terminalDispositionRef || + !sameTaskToPrRef(previous.terminalDispositionRef, current.terminalDispositionRef)) + ) { + addIssue( + "terminalDispositionRef", + "A durable terminal-disposition owner fact cannot change or disappear" + ); + } + + for (const [path, left, right] of [ + ["merge", previous.merge?.outcome ? previous.merge : undefined, current.merge], + ["cancellation", previous.cancellation, current.cancellation], + ["cleanup", previous.cleanup?.outcome ? previous.cleanup : undefined, current.cleanup], + ["rollback", previous.rollback?.outcome ? previous.rollback : undefined, current.rollback] + ] as const) { + if (left && JSON.stringify(left) !== JSON.stringify(right)) { + addIssue(path, "Complete immutable terminal owner facts cannot change or disappear"); + } + } + + return issues.length === 0 ? { success: true, issues: [] } : { success: false, issues }; +} + +export function validateTaskToPrAdapterCoreEquivalence( + localInput: unknown, + cloudInput: unknown +): TaskToPrTransitionResult { + const issues: TaskToPrTransitionIssue[] = []; + const parsedLocal = TaskToPrProjectionSchema.safeParse(localInput); + const parsedCloud = TaskToPrProjectionSchema.safeParse(cloudInput); + if (!parsedLocal.success) { + issues.push(...taskToPrParseIssues("local", parsedLocal.error.issues)); + } + if (!parsedCloud.success) { + issues.push(...taskToPrParseIssues("cloud", parsedCloud.error.issues)); + } + if (!parsedLocal.success || !parsedCloud.success) { + return { success: false, issues }; + } + if ( + parsedLocal.data.adapterExtensions.length === 0 || + parsedLocal.data.adapterExtensions.some((extension) => extension.mode !== "local") + ) { + issues.push({ + path: "local.adapterExtensions", + message: "The first adapter projection must contain one or more local-only extensions" + }); + } + if ( + parsedCloud.data.adapterExtensions.length === 0 || + parsedCloud.data.adapterExtensions.some((extension) => extension.mode !== "cloud") + ) { + issues.push({ + path: "cloud.adapterExtensions", + message: "The second adapter projection must contain one or more cloud-only extensions" + }); + } + if (issues.length > 0) { + return { success: false, issues }; + } + const { adapterExtensions: _localExtensions, ...localCore } = parsedLocal.data; + const { adapterExtensions: _cloudExtensions, ...cloudCore } = parsedCloud.data; + if (JSON.stringify(localCore) !== JSON.stringify(cloudCore)) { + return { + success: false, + issues: [ + { + path: "core", + message: "Local and cloud adapters must serialize byte-equivalent task-to-PR core projections" + } + ] + }; + } + return { success: true, issues: [] }; +} + export const TrajectoryEventSchema = z .object({ id: z.string().min(1), @@ -2729,6 +5616,11 @@ export const ContractSchemaRegistry = { [SCHEMA_IDS.resourceRef]: ResourceRefSchema, [SCHEMA_IDS.evidenceRef]: EvidenceRefSchema, [SCHEMA_IDS.workRun]: WorkRunSchema, + [SCHEMA_IDS.taskToPrProjection]: TaskToPrProjectionSchema as z.ZodType< + TaskToPrProjection, + z.ZodTypeDef, + TaskToPrProjectionInput + >, [SCHEMA_IDS.decisionEnvelope]: DecisionEnvelopeSchema, [SCHEMA_IDS.costEstimate]: CostEstimateSchema, [SCHEMA_IDS.capabilityCard]: CapabilityCardSchema, @@ -2764,6 +5656,7 @@ export type ContractBySchemaId = { [SCHEMA_IDS.resourceRef]: ResourceRef; [SCHEMA_IDS.evidenceRef]: EvidenceRef; [SCHEMA_IDS.workRun]: WorkRun; + [SCHEMA_IDS.taskToPrProjection]: TaskToPrProjection; [SCHEMA_IDS.decisionEnvelope]: DecisionEnvelope; [SCHEMA_IDS.costEstimate]: CostEstimate; [SCHEMA_IDS.capabilityCard]: CapabilityCard; @@ -2796,6 +5689,7 @@ export type ActorRefInput = z.input; export type ResourceRefInput = z.input; export type EvidenceRefInput = z.input; export type WorkRunInput = z.input; +export type TaskToPrProjectionInput = z.input; export type DecisionEnvelopeInput = z.input; export type CostEstimateInput = z.input; export type CapabilityCardInput = z.input; @@ -2831,6 +5725,7 @@ export type ContractInputBySchemaId = { [SCHEMA_IDS.resourceRef]: ResourceRefInput; [SCHEMA_IDS.evidenceRef]: EvidenceRefInput; [SCHEMA_IDS.workRun]: WorkRunInput; + [SCHEMA_IDS.taskToPrProjection]: TaskToPrProjectionInput; [SCHEMA_IDS.decisionEnvelope]: DecisionEnvelopeInput; [SCHEMA_IDS.costEstimate]: CostEstimateInput; [SCHEMA_IDS.capabilityCard]: CapabilityCardInput; diff --git a/tests/task-to-pr-projection.test.ts b/tests/task-to-pr-projection.test.ts new file mode 100644 index 0000000..5264e92 --- /dev/null +++ b/tests/task-to-pr-projection.test.ts @@ -0,0 +1,4740 @@ +import { describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; +import { + SCHEMA_IDS, + TASK_TO_PR_ROLE_AUTHORITIES, + type TaskToPrProjection, + type TaskToPrRef, + TaskToPrAdapterExtensionSchema, + TaskToPrHandoffSchema, + TaskToPrProjectionSchema, + TaskToPrRecoverySchema, + TaskToPrRefSchema, + WorkRunSchema, + deriveTaskToPrEvidenceId, + deriveTaskToPrIdentityDigest, + deriveTaskToPrRefId, + parseContract, + parseEmbeddedContract, + validateContract, + validateTaskToPrAdapterCoreEquivalence, + validateTaskToPrProjectionTransition +} from "../src"; + +type TaskToPrRole = keyof typeof TASK_TO_PR_ROLE_AUTHORITIES; +type TaskToPrAuthority = (typeof TASK_TO_PR_ROLE_AUTHORITIES)[TaskToPrRole][number]; +type Redaction = "none" | "partial" | "full"; + +const createdAt = "2026-07-23T15:10:00.000Z"; + +function sha(seed: string): string { + return seed.repeat(64); +} + +function opaque32(seed: string): string { + return createHash("sha256").update(seed, "utf8").digest("hex").slice(0, 32); +} + +function projectionId(seed: string): string { + return `task_to_pr_projection:opaque-${opaque32(seed)}`; +} + +function attemptNonce(seed: string): string { + return `attempt_nonce:opaque-${opaque32(seed)}`; +} + +function ref(role: TaskToPrRole, authority: TaskToPrAuthority, seed: string, redaction: Redaction = "none") { + const digest = createHash("sha256").update(`${role}:${authority}:${seed}`, "utf8").digest("hex"); + return { + role, + authority, + id: deriveTaskToPrRefId(role, authority, digest), + digest, + redaction + }; +} + +function evidence(id: string, seed: string, redaction: "partial" | "full" = "full") { + const digest = sha(seed); + return { id: deriveTaskToPrEvidenceId(digest), digest, redaction }; +} + +function head(seed: string) { + return { algorithm: "sha1" as const, value: seed.repeat(40) }; +} + +function withActiveProvenance>( + projection: T +): Omit & { provenanceLedger: TaskToPrProjection["provenanceLedger"] } { + const terminalStates = new Set([ + "merged", + "closed_unmerged", + "failed", + "blocked", + "cancelled", + "cleanup_complete", + "rolled_back" + ]); + const normalized = { + ...projection, + terminalDispositionRef: terminalStates.has(projection.state) + ? projection.terminalDispositionRef + : undefined, + merge: + projection.merge && projection.state === "merge_ready" + ? { + ...projection.merge, + guard: { ...projection.merge.guard, decision: "eligible" } + } + : projection.merge + }; + const active = [ + { category: "projection_id", projectionId: normalized.id }, + { category: "work_run", ref: normalized.workRunRef }, + { category: "attempt", ref: normalized.attempt.ref }, + { category: "admission", ref: normalized.attempt.admissionRef }, + { category: "worker_actor", ref: normalized.attempt.workerActorRef }, + { category: "worker_assignment", ref: normalized.attempt.workerRef }, + { category: "attempt_nonce", nonce: normalized.attempt.nonce }, + { category: "runtime", ref: normalized.attempt.runtimeRef }, + { category: "writer_generation", ref: normalized.attempt.writerGenerationRef }, + { category: "writer_lease", ref: normalized.attempt.writerLeaseRef }, + { category: "writer_fence", ref: normalized.attempt.writerFenceRef }, + { category: "provider_profile", ref: normalized.attempt.providerProfileRef }, + { category: "provider_route", ref: normalized.attempt.providerRouteRef }, + { category: "replay_cursor", ref: normalized.events.replayCursorRef }, + { + category: "replay_prefix", + sequence: normalized.events.sequence, + prefixDigest: normalized.events.prefixDigest + }, + { category: "repair_state", ref: normalized.repair.ref }, + ...(normalized.repair.latestRepairRef + ? [{ category: "latest_repair", ref: normalized.repair.latestRepairRef }] + : []), + ...(normalized.handoff ? [{ category: "handoff", ref: normalized.handoff.ref }] : []), + ...(normalized.recovery ? [{ category: "recovery", ref: normalized.recovery.ref }] : []), + ...(normalized.exactHead + ? [ + { + category: "equality_proof", + ref: normalized.exactHead.equalityProofRef, + base: normalized.exactHead.expectedBase, + head: normalized.exactHead.localHead + }, + ...normalized.exactHead.ciProofBundleRefs.map((proofRef: TaskToPrRef) => ({ + category: "ci_proof", + ref: proofRef, + base: normalized.exactHead.expectedBase, + head: normalized.exactHead.localHead + })) + ] + : []), + ...((normalized.reviews ?? []) as Array<{ + ref: TaskToPrRef; + reviewRunRef: TaskToPrRef; + proofBundleRef: TaskToPrRef; + base: ReturnType; + head: ReturnType; + }>).flatMap((review) => [ + { category: "review_proof", ref: review.proofBundleRef, base: review.base, head: review.head }, + { category: "review_record", ref: review.ref, base: review.base, head: review.head }, + { category: "review_run", ref: review.reviewRunRef, base: review.base, head: review.head } + ]), + ...(normalized.merge + ? [ + { category: "merge_guard", ref: normalized.merge.guard.ref }, + { + category: "provider_guard_receipt", + ref: normalized.merge.guard.providerGuardReceiptRef, + base: normalized.merge.guard.expectedBase, + head: normalized.merge.guard.expectedHead + } + ] + : []), + ...(normalized.cleanup + ? [{ category: "cleanup_eligibility", ref: normalized.cleanup.eligibility.ref }] + : []), + ...(normalized.rollback + ? [{ category: "rollback_plan", ref: normalized.rollback.plan.ref }] + : []), + ...(normalized.terminalDispositionRef + ? [{ category: "terminal_disposition", ref: normalized.terminalDispositionRef }] + : []) + ]; + const provenanceLedger = [...(normalized.provenanceLedger ?? [])]; + for (const entry of active) { + if (!provenanceLedger.some((existing: unknown) => JSON.stringify(existing) === JSON.stringify(entry))) { + provenanceLedger.push(entry); + } + } + return { ...normalized, provenanceLedger } as Omit & { + provenanceLedger: TaskToPrProjection["provenanceLedger"]; + }; +} + +function parseProjection>(projection: T): TaskToPrProjection { + return TaskToPrProjectionSchema.parse(withActiveProvenance(projection)); +} + +function validProjection() { + const reviewedHead = head("a"); + const pullRequestRef = ref("pull_request", "todos", "b"); + const currentGeneration = ref("writer_generation", "todos", "c"); + const currentAttempt = ref("attempt", "todos", "d"); + const reviewRef = ref("review", "review", "e"); + const proofBundleRef = ref("proof_bundle", "review", "f"); + const headEqualityProofRef = ref("proof_bundle", "review", "0"); + const ciProofBundleRef = ref("proof_bundle", "review", "1"); + const rootRequestRef = ref("root_request", "todos", "4"); + const prGroupRef = ref("pr_group", "todos", "5"); + const leafTaskRef = ref("leaf_task", "todos", "6"); + const repoRef = ref("repo", "repos", "b"); + const worktreeRef = ref("worktree", "repos", "c", "partial"); + const branchRef = ref("branch", "repos", "d"); + const baseHead = head("e"); + const frozenScopeDigest = sha("2"); + const identityDigest = deriveTaskToPrIdentityDigest({ + canonicalizationVersion: 2, + rootRequestRef, + prGroupRef, + leafTaskRef, + repoRef, + worktreeRef, + branchRef, + baseHead, + frozenScopeDigest + }); + + return withActiveProvenance({ + schema: SCHEMA_IDS.taskToPrProjection, + id: projectionId("valid-projection"), + createdAt, + canonicalizationVersion: 2, + identityDigest, + frozenScopeDigest, + state: "cleanup_complete", + workRunRef: ref("work_run", "codewith", "3"), + rootRequestRef, + prGroupRef, + leafTaskRef, + attempt: { + ref: currentAttempt, + nonce: attemptNonce("valid-attempt"), + admissionRef: ref("admission", "codewith", "1", "partial"), + admissionWriterGenerationRef: currentGeneration, + workerActorRef: ref("worker_actor", "codewith", "stable-worker", "partial"), + workerRef: ref("worker", "codewith", "2", "partial"), + runtimeRef: ref("runtime", "codewith", "3", "partial"), + writerGenerationRef: currentGeneration, + writerLeaseRef: ref("writer_lease", "repos", "7", "partial"), + writerFenceRef: ref("writer_fence", "repos", "8", "full"), + providerProfileRef: ref("provider_profile", "codewith", "9", "full"), + providerRouteRef: ref("provider_route", "codewith", "a", "partial") + }, + repository: { + repoRef, + worktreeRef, + branchRef, + baseHead, + branchHead: reviewedHead + }, + events: { + streamRef: ref("event_stream", "todos", "e"), + replayCursorRef: ref("replay_cursor", "todos", "f"), + sequence: 42, + prefixDigest: sha("0") + }, + openLoopsInvocationRef: ref("openloops_invocation", "openloops", "1", "partial"), + pullRequestRef, + handoff: { + ref: ref("handoff", "todos", "2"), + previousAttemptRef: ref("attempt", "todos", "1"), + nextAttemptRef: currentAttempt, + previousWriterGenerationRef: ref("writer_generation", "todos", "3"), + nextWriterGenerationRef: currentGeneration, + stoppedWorkRunRef: ref("work_run", "codewith", "4"), + stopEvidenceRef: evidence("ev_prior_worker_stopped", "5"), + leaseRevocationEvidenceRef: evidence("ev_prior_lease_revoked", "6") + }, + exactHead: { + pullRequestRef, + remoteBranchRef: ref("branch", "repos", "d"), + expectedBase: baseHead, + providerPullRequestBase: baseHead, + localHead: reviewedHead, + remoteHead: reviewedHead, + providerPullRequestHead: reviewedHead, + equalityProofRef: headEqualityProofRef, + ciProofBundleRefs: [ciProofBundleRef], + verifiedAt: createdAt + }, + reviews: [ + { + ref: reviewRef, + pullRequestRef, + base: baseHead, + head: reviewedHead, + reviewerRef: ref("reviewer", "review", "7"), + reviewRunRef: ref("review_run", "review", "8"), + proofBundleRef, + verdict: "approved", + reviewedAt: createdAt + } + ], + repair: { + ref: ref("repair_cycle", "todos", "9"), + cycle: 1, + cap: 2, + exhausted: false, + latestRepairRef: ref("repair_cycle", "todos", "a") + }, + merge: { + guard: { + ref: ref("merge_guard", "todos", "b"), + pullRequestRef, + expectedBase: baseHead, + expectedHead: reviewedHead, + reviewRefs: [reviewRef], + proofBundleRefs: [proofBundleRef, headEqualityProofRef, ciProofBundleRef], + operatorRef: ref("merge_operator", "merge_provider", "c", "partial"), + operatorRunRef: ref("merge_operator_run", "merge_provider", "d", "partial"), + providerGuardReceiptRef: ref("merge_guard_receipt", "merge_provider", "c", "full"), + mechanism: "compare_and_swap", + decision: "consumed", + evaluatedAt: createdAt + }, + outcome: { + ref: ref("merge_outcome", "merge_provider", "d", "partial"), + guardRef: ref("merge_guard", "todos", "b"), + pullRequestRef, + expectedBase: baseHead, + observedBase: baseHead, + expectedHead: reviewedHead, + observedHead: reviewedHead, + status: "merged", + mergeCommitRef: ref("commit", "repos", "e"), + finishedAt: createdAt, + evidenceRefs: [evidence("ev_merge_cas", "f")] + } + }, + terminalDispositionRef: ref("terminal_disposition", "todos", "terminal"), + cleanup: { + eligibility: { + ref: ref("cleanup_eligibility", "repos", "6"), + status: "eligible", + targetWorktreeRef: ref("worktree", "repos", "c", "partial"), + eventCursorRef: ref("replay_cursor", "todos", "f"), + terminalDispositionRef: ref("terminal_disposition", "todos", "terminal"), + writerLeaseRef: ref("writer_lease", "repos", "7", "partial"), + leaseRevocationEvidenceRef: evidence("ev_cleanup_lease_revoked", "1"), + consumedEventEvidenceRef: evidence("ev_cleanup_event_consumed", "2"), + evaluatedAt: createdAt, + evidenceRefs: [evidence("ev_cleanup_gate", "7")] + }, + outcome: { + ref: ref("cleanup_outcome", "repos", "8"), + eligibilityRef: ref("cleanup_eligibility", "repos", "6"), + targetWorktreeRef: ref("worktree", "repos", "c", "partial"), + status: "deleted", + finishedAt: createdAt, + evidenceRefs: [evidence("ev_cleanup_deleted", "9")] + } + }, + provenanceLedger: [], + adapterExtensions: [ + { + mode: "local", + schema: "hasna.task_to_pr_adapter_extension.v1", + ref: ref("adapter_extension", "adapter", "a", "partial"), + digest: sha("b") + }, + { + mode: "cloud", + schema: "hasna.task_to_pr_adapter_extension.v1", + ref: ref("adapter_extension", "adapter", "c", "full"), + digest: sha("d") + } + ], + evidenceRefs: [evidence("ev_projection", "e")] + }); +} + +function validRecoveryProjection() { + const projection = validProjection(); + return { + ...projection, + handoff: undefined, + recovery: { + ref: ref("recovery", "todos", "7"), + priorAttemptRef: ref("attempt", "todos", "1"), + priorWriterGenerationRef: ref("writer_generation", "todos", "2"), + priorWorkRunRef: ref("work_run", "codewith", "4"), + successorAttemptNonce: projection.attempt.nonce, + successorWriterGenerationRef: projection.attempt.writerGenerationRef, + preservedStateRefs: [ + ref("work_run", "codewith", "4"), + projection.rootRequestRef, + projection.prGroupRef, + projection.leafTaskRef, + projection.repository.repoRef, + projection.repository.worktreeRef, + projection.repository.branchRef, + projection.events.streamRef, + projection.pullRequestRef + ], + stopEvidenceRef: evidence("ev_recovery_stop", "4"), + leaseRevocationEvidenceRef: evidence("ev_recovery_revoke", "5") + }, + provenanceLedger: [ + ...projection.provenanceLedger, + { category: "recovery" as const, ref: ref("recovery", "todos", "7") } + ] + }; +} + +describe("task-to-PR projection v1", () => { + test("parses and round-trips the strict provider-neutral projection", () => { + const input = validProjection(); + const parsed = TaskToPrProjectionSchema.parse(input); + const roundTrip = parseEmbeddedContract(JSON.parse(JSON.stringify(parsed))); + + expect(roundTrip).toEqual(parsed); + expect(parseContract(SCHEMA_IDS.taskToPrProjection, input)).toEqual(parsed); + expect(parsed.workRunRef.role).toBe("work_run"); + expect(parsed.events.sequence).toBe(42); + }); + + test("derives and verifies the non-circular canonical root/PR-group/leaf/repo/base/scope binding", () => { + const projection = validProjection(); + expect( + deriveTaskToPrIdentityDigest({ + canonicalizationVersion: 2, + rootRequestRef: projection.rootRequestRef, + prGroupRef: projection.prGroupRef, + leafTaskRef: projection.leafTaskRef, + repoRef: projection.repository.repoRef, + worktreeRef: projection.repository.worktreeRef, + branchRef: projection.repository.branchRef, + baseHead: projection.repository.baseHead, + frozenScopeDigest: projection.frozenScopeDigest + }) + ).toBe(projection.identityDigest); + expect(projection.prGroupRef.digest).not.toBe(projection.identityDigest); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + identityDigest: sha("0") + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + leafTaskRef: { ...projection.leafTaskRef, id: "leaf_task_changed" } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + prGroupRef: { ...projection.prGroupRef, id: "pr_group:opaque-changed" } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + prGroupRef: { ...projection.prGroupRef, digest: sha("0") } + }).success + ).toBe(false); + }); + + test("enforces the exhaustive role-to-authority matrix", () => { + const expectedRoles = [ + "work_run", + "root_request", + "pr_group", + "leaf_task", + "attempt", + "writer_generation", + "writer_lease", + "writer_fence", + "provider_profile", + "provider_route", + "admission", + "worker_actor", + "worker", + "runtime", + "repo", + "worktree", + "branch", + "event_stream", + "replay_cursor", + "handoff", + "pull_request", + "commit", + "review", + "reviewer", + "review_run", + "proof_bundle", + "repair_cycle", + "merge_guard", + "merge_operator", + "merge_operator_run", + "merge_guard_receipt", + "merge_outcome", + "recovery", + "cancellation", + "cleanup_eligibility", + "cleanup_outcome", + "rollback_plan", + "rollback_outcome", + "terminal_disposition", + "openloops_invocation", + "adapter_extension" + ]; + expect(Object.keys(TASK_TO_PR_ROLE_AUTHORITIES)).toEqual(expectedRoles); + + const redactedRoles = new Set([ + "writer_lease", + "writer_fence", + "provider_profile", + "provider_route", + "admission", + "worker_actor", + "worker", + "runtime", + "worktree", + "merge_operator", + "merge_operator_run", + "merge_guard_receipt", + "merge_outcome", + "openloops_invocation", + "adapter_extension" + ]); + for (const [role, authorities] of Object.entries(TASK_TO_PR_ROLE_AUTHORITIES)) { + for (const authority of authorities) { + expect( + TaskToPrRefSchema.safeParse(ref(role as TaskToPrRole, authority, "a", redactedRoles.has(role as TaskToPrRole) ? "full" : "none")) + .success + ).toBe(true); + } + const invalidAuthority = (authorities as readonly string[]).includes("todos") ? "codewith" : "todos"; + expect( + TaskToPrRefSchema.safeParse({ + ...ref(role as TaskToPrRole, authorities[0], "b", redactedRoles.has(role as TaskToPrRole) ? "full" : "none"), + authority: invalidAuthority + }).success + ).toBe(false); + } + }); + + test("exposes an immutable role-to-authority matrix without changing validation", () => { + const validRootRef = ref("root_request", "todos", "a"); + expect(TaskToPrRefSchema.safeParse(validRootRef).success).toBe(true); + expect(Object.isFrozen(TASK_TO_PR_ROLE_AUTHORITIES)).toBe(true); + expect(Object.isFrozen(TASK_TO_PR_ROLE_AUTHORITIES.root_request)).toBe(true); + expect(Reflect.set(TASK_TO_PR_ROLE_AUTHORITIES, "root_request", Object.freeze(["codewith"]))).toBe(false); + expect(Reflect.set(TASK_TO_PR_ROLE_AUTHORITIES.root_request, 0, "codewith")).toBe(false); + expect(TASK_TO_PR_ROLE_AUTHORITIES.root_request).toEqual(["todos"]); + expect(TaskToPrRefSchema.safeParse(validRootRef).success).toBe(true); + }); + + test("keeps singular canonical identities required and rejects embedded mutable payloads", () => { + const missingRoot = validProjection() as Record; + delete missingRoot.rootRequestRef; + expect(TaskToPrProjectionSchema.safeParse(missingRoot).success).toBe(false); + + const embeddedTask = validProjection(); + const invalid = { + ...embeddedTask, + leafTaskRef: { + ...embeddedTask.leafTaskRef, + title: "mutable task payload", + status: "in_progress" + } + }; + expect(TaskToPrProjectionSchema.safeParse(invalid).success).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...validProjection(), + rootRequestRefs: [validProjection().rootRequestRef] + }).success + ).toBe(false); + }); + + test("rejects raw account, credential, token, fence, and adapter payload fields", () => { + const projection = validProjection(); + const rawProfile = { + ...projection, + attempt: { + ...projection.attempt, + providerProfileRef: { + ...projection.attempt.providerProfileRef, + accountId: "provider-account-123" + } + } + }; + expect(TaskToPrProjectionSchema.safeParse(rawProfile).success).toBe(false); + + const rawFenceInvalidFixture = { + ...projection, + attempt: { + ...projection.attempt, + fenceToken: "raw-fence-value" + } + }; + expect(TaskToPrProjectionSchema.safeParse(rawFenceInvalidFixture).success).toBe(false); + + const rawExtension = { + ...projection, + adapterExtensions: [ + { + ...projection.adapterExtensions[0], + provider: "github", + credentials: { token: "raw-token" }, + payload: { mutable: true } + } + ] + }; + expect(TaskToPrProjectionSchema.safeParse(rawExtension).success).toBe(false); + + const secretBearingIds = [ + ["provider_profile", "codewith", "provider_profile:opaque-alice@example.com"], + ["provider_route", "codewith", "provider_route:opaque-provider.route?token=value"], + ["writer_fence", "repos", "writer_fence:opaque-github_pat_example"], + ["writer_fence", "repos", "writer_fence:opaque-f.ence447"], + ["writer_lease", "repos", "writer_lease:opaque-raw-fence-value"], + ["runtime", "codewith", "runtime:opaque------BEGIN-PRIVATE-KEY"], + ["merge_operator", "merge_provider", "merge_operator:opaque-Bearer-token"], + ["root_request", "todos", "root_request:opaque-acc.ount013"], + ["branch", "repos", "branch:opaque-raw-fence-447"] + ] as const; + for (const [role, authority, id] of secretBearingIds) { + expect( + TaskToPrRefSchema.safeParse({ + ...ref(role, authority, "a", "full"), + id + }).success + ).toBe(false); + } + const delimiterNormalizedGithubPatRef: TaskToPrRef = { + role: "writer_fence", + authority: "repos", + id: "writer_fence:opaque-github-pat-example", + digest: sha("a"), + redaction: "full" + }; + expect(TaskToPrRefSchema.safeParse(delimiterNormalizedGithubPatRef).success).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + evidenceRefs: [ + { + ...evidence("ev_secret", "a"), + uri: "https://user:password@example.com/artifact" + } + ] + }).success + ).toBe(false); + + for (const invalidId of [ + "task_to_pr_projection:account013", + "task_to_pr_projection:raw-fence-447", + "task_to_pr_projection:https-provider", + "task_to_pr_projection:opaque-" + ]) { + expect(TaskToPrProjectionSchema.safeParse({ ...projection, id: invalidId }).success).toBe(false); + } + for (const invalidNonce of [ + "attempt_nonce:account013", + "attempt_nonce:raw-fence-447", + "attempt_nonce:opaque-to.ken447", + "raw-untyped-nonce", + "attempt_nonce:opaque-" + ]) { + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + attempt: { ...projection.attempt, nonce: invalidNonce } + }).success + ).toBe(false); + } + expect( + TaskToPrRefSchema.safeParse({ + ...ref("root_request", "todos", "a"), + id: "root_request:opaque-" + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + adapterExtensions: [ + { + ...projection.adapterExtensions[0], + schema: "hasna.task_to_pr_adapter_acc_ount.v1" + } + ] + }).success + ).toBe(false); + for (const rawEvidenceId of ["evidence:opaque-account013", "evidence:opaque-raw-fence-447", "evidence:untyped"]) { + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + evidenceRefs: [{ ...evidence("ev_safe", "a"), id: rawEvidenceId }] + }).success + ).toBe(false); + } + + expect( + TaskToPrProjectionSchema.safeParse( + withActiveProvenance({ + ...projection, + id: projectionId("valid-projection"), + attempt: { + ...projection.attempt, + nonce: attemptNonce("owner-record") + }, + provenanceLedger: projection.provenanceLedger.filter( + (entry) => entry.category !== "attempt_nonce" + ) + }) + ).success + ).toBe(true); + }); + + test("requires redaction for opaque profile, route, lease, fence, worktree, and provider refs", () => { + const sensitiveRoles: TaskToPrRole[] = [ + "writer_lease", + "writer_fence", + "provider_profile", + "provider_route", + "admission", + "worker", + "runtime", + "worktree", + "merge_operator", + "merge_operator_run", + "merge_guard_receipt", + "merge_outcome", + "openloops_invocation", + "adapter_extension" + ]; + for (const role of sensitiveRoles) { + const authority = TASK_TO_PR_ROLE_AUTHORITIES[role][0]; + expect(TaskToPrRefSchema.safeParse(ref(role, authority, "a", "none")).success).toBe(false); + expect(TaskToPrRefSchema.safeParse(ref(role, authority, "a", "partial")).success).toBe(true); + } + }); + + test("binds replay cursors to a non-negative sequence and prefix digest", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + events: { ...projection.events, sequence: -1 } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + events: { ...projection.events, prefixDigest: "not-a-digest" } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + events: { + ...projection.events, + replayCursorRef: ref("event_stream", "todos", "a") + } + }).success + ).toBe(false); + }); + + test("prevents review and merge-CAS from floating away from the exact PR head", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + remoteBranchRef: ref("branch", "repos", "f") + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + remoteHead: head("b") + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + ciProofBundleRefs: [] + } + }).success + ).toBe(false); + const driftedGuard = { + ...projection, + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + expectedHead: head("b") + } + } + }; + expect(TaskToPrProjectionSchema.safeParse(driftedGuard).success).toBe(false); + + const driftedMerge = { + ...projection, + merge: { + ...projection.merge, + outcome: { + ...projection.merge.outcome, + observedHead: head("b") + } + } + }; + expect(TaskToPrProjectionSchema.safeParse(driftedMerge).success).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "closed_unmerged", + cleanup: undefined, + merge: { + ...projection.merge, + outcome: { + ...projection.merge.outcome, + status: "refused", + observedHead: head("b"), + mergeCommitRef: undefined + } + } + }).success + ).toBe(false); + + const refusedDrift = { + ...projection, + state: "closed_unmerged", + cleanup: undefined, + merge: { + ...projection.merge, + outcome: { + ...projection.merge.outcome, + status: "head_drift", + observedHead: head("b"), + mergeCommitRef: undefined + } + } + }; + expect(TaskToPrProjectionSchema.safeParse(refusedDrift).success).toBe(true); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + ciProofBundleRefs: [ + projection.exactHead.ciProofBundleRefs[0], + { + ...projection.exactHead.ciProofBundleRefs[0], + digest: sha("9") + } + ] + } + }).success + ).toBe(false); + + const duplicateCiDigest = { + ...projection.exactHead.ciProofBundleRefs[0], + id: "proof_bundle:opaque-ci-duplicate-digest" + }; + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + ciProofBundleRefs: [...projection.exactHead.ciProofBundleRefs, duplicateCiDigest] + }, + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + proofBundleRefs: [...projection.merge.guard.proofBundleRefs, duplicateCiDigest] + } + } + }).success + ).toBe(false); + + const ciWithEqualityDigest = { + ...projection.exactHead.ciProofBundleRefs[0], + digest: projection.exactHead.equalityProofRef.digest + }; + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + ciProofBundleRefs: [ciWithEqualityDigest] + }, + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + proofBundleRefs: projection.merge.guard.proofBundleRefs.map((proofRef) => + proofRef.id === projection.exactHead.ciProofBundleRefs[0]!.id ? ciWithEqualityDigest : proofRef + ) + } + } + }).success + ).toBe(false); + }); + + test("keeps equality, CI, and review proof obligations globally distinct by identity and digest", () => { + const projection = validProjection(); + const reviewOnly = parseProjection({ + ...projection, + state: "reviewing", + merge: undefined, + cleanup: undefined + }); + const equalityProof = reviewOnly.exactHead!.equalityProofRef; + const sameIdentityAlias: TaskToPrProjection = { + ...reviewOnly, + reviews: [ + { + ...reviewOnly.reviews[0]!, + proofBundleRef: { + ...reviewOnly.reviews[0]!.proofBundleRef, + id: equalityProof.id + } + } + ] + }; + expect(TaskToPrProjectionSchema.safeParse(sameIdentityAlias).success).toBe(false); + + const sameDigestAlias: TaskToPrProjection = { + ...reviewOnly, + reviews: [ + { + ...reviewOnly.reviews[0]!, + proofBundleRef: { + ...reviewOnly.reviews[0]!.proofBundleRef, + digest: equalityProof.digest + } + } + ] + }; + expect(TaskToPrProjectionSchema.safeParse(sameDigestAlias).success).toBe(false); + + const eligible = parseProjection({ + ...projection, + state: "merge_ready", + merge: { + guard: projection.merge.guard + }, + cleanup: undefined + }); + const ciProof = eligible.exactHead!.ciProofBundleRefs[0]!; + const originalReviewProof = eligible.reviews[0]!.proofBundleRef; + const sharedCiReviewProof: TaskToPrProjection = { + ...eligible, + reviews: [ + { + ...eligible.reviews[0]!, + proofBundleRef: ciProof + } + ], + merge: { + guard: { + ...eligible.merge!.guard, + proofBundleRefs: eligible.merge!.guard.proofBundleRefs.filter( + (proofRef) => proofRef.id !== originalReviewProof.id + ) + } + } + }; + expect(TaskToPrProjectionSchema.safeParse(sharedCiReviewProof).success).toBe(false); + + const reviewProofWithCiDigest = { + ...originalReviewProof, + digest: ciProof.digest + }; + const aliasedEligibleGuard: TaskToPrProjection = { + ...eligible, + reviews: [ + { + ...eligible.reviews[0]!, + proofBundleRef: reviewProofWithCiDigest + } + ], + merge: { + guard: { + ...eligible.merge!.guard, + proofBundleRefs: eligible.merge!.guard.proofBundleRefs.map((proofRef) => + proofRef.id === originalReviewProof.id ? reviewProofWithCiDigest : proofRef + ) + } + } + }; + expect(TaskToPrProjectionSchema.safeParse(aliasedEligibleGuard).success).toBe(false); + }); + + test("supports multiple exact-head reviews and keeps worker, reviewers, and merge operator distinct", () => { + const projection = validProjection(); + const secondReview = { + ...projection.reviews[0]!, + ref: ref("review", "review", "3"), + reviewerRef: ref("reviewer", "review", "5"), + reviewRunRef: ref("review_run", "review", "4"), + proofBundleRef: ref("proof_bundle", "review", "5") + }; + const reviewed = withActiveProvenance({ + ...projection, + reviews: [...projection.reviews, secondReview], + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + reviewRefs: [...projection.merge.guard.reviewRefs, secondReview.ref], + proofBundleRefs: [...projection.merge.guard.proofBundleRefs, secondReview.proofBundleRef] + } + } + }); + expect(TaskToPrProjectionSchema.safeParse(reviewed).success).toBe(true); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewed, + merge: { + ...reviewed.merge, + guard: { + ...reviewed.merge.guard, + reviewRefs: [...reviewed.merge.guard.reviewRefs, ref("review", "review", "9")] + } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewed, + merge: { + ...reviewed.merge, + guard: { + ...reviewed.merge.guard, + reviewRefs: [reviewed.reviews[0]!.ref] + } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewed, + merge: { + ...reviewed.merge, + guard: { + ...reviewed.merge.guard, + reviewRefs: [...reviewed.merge.guard.reviewRefs].reverse() + } + } + }).success + ).toBe(true); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewed, + reviews: [ + reviewed.reviews[0], + { + ...secondReview, + reviewerRef: reviewed.reviews[0]!.reviewerRef + } + ] + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewed, + reviews: [ + reviewed.reviews[0], + { + ...secondReview, + ref: { + ...secondReview.ref, + digest: reviewed.reviews[0]!.ref.digest + } + } + ] + }).success + ).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + reviews: [ + { + ...projection.reviews[0]!, + reviewerRef: { ...projection.reviews[0]!.reviewerRef, digest: projection.attempt.workerRef.digest } + } + ] + }).success + ).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + reviews: [ + { + ...projection.reviews[0]!, + reviewRunRef: { ...projection.reviews[0]!.reviewRunRef, digest: projection.attempt.runtimeRef.digest } + } + ] + }).success + ).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + operatorRef: { ...projection.merge.guard.operatorRef, digest: projection.reviews[0]!.reviewerRef.digest } + } + } + }).success + ).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewed, + reviews: [ + reviewed.reviews[0], + { + ...secondReview, + reviewerRef: { ...secondReview.reviewerRef, id: reviewed.reviews[0]!.reviewerRef.id } + } + ] + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewed, + reviews: [ + reviewed.reviews[0], + { + ...secondReview, + reviewerRef: { ...secondReview.reviewerRef, digest: reviewed.reviews[0]!.reviewerRef.digest } + } + ] + }).success + ).toBe(false); + + const reviewOnly = { + ...reviewed, + state: "reviewing", + merge: undefined, + cleanup: undefined + }; + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewOnly, + reviews: [ + reviewOnly.reviews[0], + { + ...reviewOnly.reviews[1], + proofBundleRef: { + ...reviewOnly.reviews[1]!.proofBundleRef, + id: reviewOnly.reviews[0]!.proofBundleRef.id + } + } + ] + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewOnly, + reviews: [ + reviewOnly.reviews[0], + { + ...reviewOnly.reviews[1], + proofBundleRef: { + ...reviewOnly.reviews[1]!.proofBundleRef, + digest: reviewOnly.reviews[0]!.proofBundleRef.digest + } + } + ] + }).success + ).toBe(false); + + const duplicateGuardDigest = { + ...projection.merge.guard.proofBundleRefs[0], + id: "proof_bundle:opaque-guard-duplicate-digest" + }; + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + proofBundleRefs: [...projection.merge.guard.proofBundleRefs, duplicateGuardDigest] + } + } + }).success + ).toBe(false); + const duplicateGuardCanonicalId = { + ...projection.merge.guard.proofBundleRefs[0], + digest: sha("9") + }; + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + proofBundleRefs: [...projection.merge.guard.proofBundleRefs, duplicateGuardCanonicalId] + } + } + }).success + ).toBe(false); + }); + + test("enforces exact-head, review, guard, outcome, cleanup, and rollback chronology", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + verifiedAt: "2026-07-23T15:09:59.000Z" + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + reviews: [ + { + ...projection.reviews[0]!, + reviewedAt: "2026-07-23T15:09:59.000Z" + } + ] + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + merge: { + ...projection.merge, + guard: { + ...projection.merge.guard, + evaluatedAt: "2026-07-23T15:09:59.000Z" + } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + cleanup: { + ...projection.cleanup, + outcome: { + ...projection.cleanup.outcome, + finishedAt: "2026-07-23T15:09:59.000Z" + } + } + }).success + ).toBe(false); + }); + + test("enforces repair cap and immutable cumulative state", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + repair: { ...projection.repair, cycle: 3, exhausted: true } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + repair: { ...projection.repair, cycle: 2, exhausted: false } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + repair: { ...projection.repair, cycle: 0, exhausted: false, latestRepairRef: projection.repair.latestRepairRef } + }).success + ).toBe(false); + }); + + test("validates monotonic replay, repair, attempt, and terminal transitions", () => { + const projection = validProjection(); + const previous = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + createdAt: "2026-07-23T15:09:00.000Z", + state: "running", + pullRequestRef: undefined, + exactHead: undefined, + reviews: [], + handoff: undefined, + merge: undefined, + recovery: undefined, + cleanup: undefined, + repair: { + ref: ref("repair_cycle", "todos", "0"), + cycle: 0, + cap: 2, + exhausted: false + }, + evidenceRefs: [] + }); + const current = parseProjection({ + ...previous, + id: projectionId("valid-projection"), + createdAt, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "a"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("b") + } + }); + expect(validateTaskToPrProjectionTransition(previous, current)).toEqual({ success: true, issues: [] }); + + const replayed = { + ...current, + id: projectionId("valid-projection"), + events: { ...current.events, sequence: previous.events.sequence - 1 } + }; + expect(validateTaskToPrProjectionTransition(previous, replayed).success).toBe(false); + + const staleCursor = { + ...current, + id: projectionId("valid-projection"), + events: { + ...current.events, + replayCursorRef: previous.events.replayCursorRef + } + }; + expect(validateTaskToPrProjectionTransition(previous, staleCursor).success).toBe(false); + + const reusedCursorDigest: TaskToPrProjection = { + ...current, + id: projectionId("valid-projection"), + events: { + ...current.events, + replayCursorRef: { + ...current.events.replayCursorRef, + id: "replay_cursor:opaque-fresh-id-stale-digest", + digest: previous.events.replayCursorRef.digest + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, reusedCursorDigest).success).toBe(false); + + const stalePrefix = { + ...current, + id: projectionId("valid-projection"), + events: { + ...current.events, + prefixDigest: previous.events.prefixDigest + } + }; + expect(validateTaskToPrProjectionTransition(previous, stalePrefix).success).toBe(false); + + const partialAttemptAdvance = { + ...current, + id: projectionId("valid-projection"), + attempt: { + ...current.attempt, + nonce: attemptNonce("fresh") + } + }; + expect(validateTaskToPrProjectionTransition(previous, partialAttemptAdvance).success).toBe(false); + expect( + validateTaskToPrProjectionTransition(previous, { + ...current, + id: projectionId("valid-projection"), + attempt: { + ...current.attempt, + runtimeRef: ref("runtime", "codewith", "c", "partial") + } + }).success + ).toBe(false); + expect( + validateTaskToPrProjectionTransition(previous, { + ...current, + id: projectionId("valid-projection"), + repair: { + ...current.repair, + ref: ref("repair_cycle", "todos", "c") + } + }).success + ).toBe(false); + + expect( + validateTaskToPrProjectionTransition(previous, { + ...current, + id: projectionId("valid-projection"), + unknownMutablePayload: true + }).success + ).toBe(false); + + const mutatedTerminal = { + ...projection, + id: projectionId("valid-projection"), + merge: { + ...projection.merge, + outcome: { + ...projection.merge.outcome, + finishedAt: "2026-07-23T15:11:00.000Z" + } + }, + cleanup: { + ...projection.cleanup, + eligibility: { + ...projection.cleanup.eligibility, + evaluatedAt: "2026-07-23T15:11:00.000Z" + }, + outcome: { + ...projection.cleanup.outcome, + finishedAt: "2026-07-23T15:11:00.000Z" + } + } + }; + expect(validateTaskToPrProjectionTransition(TaskToPrProjectionSchema.parse(projection), parseProjection(mutatedTerminal)).success).toBe( + false + ); + }); + + test("requires immutable or fully rotated merge, cleanup, and rollback owner records", () => { + const projection = validProjection(); + const mergePrevious = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "merge_ready", + merge: { + guard: projection.merge.guard + }, + cleanup: undefined + }); + const mergeRotated = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...mergePrevious, + id: projectionId("valid-projection"), + events: { + ...mergePrevious.events, + replayCursorRef: ref("replay_cursor", "todos", "2"), + sequence: mergePrevious.events.sequence + 1, + prefixDigest: sha("3") + }, + merge: { + guard: { + ...mergePrevious.merge!.guard, + ref: ref("merge_guard", "todos", "a"), + mechanism: "queue_expected_head" + } + } + })); + + const cleanupPrevious = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "merged", + cleanup: { + eligibility: projection.cleanup.eligibility + } + }); + const cleanupCursorRef = ref("replay_cursor", "todos", "4"); + const cleanupRotated = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...cleanupPrevious, + id: projectionId("valid-projection"), + events: { + ...cleanupPrevious.events, + replayCursorRef: cleanupCursorRef, + sequence: cleanupPrevious.events.sequence + 1, + prefixDigest: sha("5") + }, + cleanup: { + eligibility: { + ...cleanupPrevious.cleanup!.eligibility, + ref: ref("cleanup_eligibility", "repos", "a"), + status: "preserved", + eventCursorRef: cleanupCursorRef + } + } + })); + + const rollbackPrevious = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...projection, + id: projectionId("valid-projection"), + state: "merged", + cleanup: undefined, + rollback: { + plan: { + ref: ref("rollback_plan", "todos", "a"), + targetRef: projection.merge.outcome.mergeCommitRef!, + createdAt + } + } + })); + const rollbackRotated = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...rollbackPrevious, + id: projectionId("valid-projection"), + events: { + ...rollbackPrevious.events, + replayCursorRef: ref("replay_cursor", "todos", "6"), + sequence: rollbackPrevious.events.sequence + 1, + prefixDigest: sha("7") + }, + rollback: { + plan: { + ...rollbackPrevious.rollback!.plan, + ref: ref("rollback_plan", "todos", "d"), + targetRef: rollbackPrevious.repository.branchRef + } + } + })); + + const cases = [ + { + name: "merge guard mechanism", + path: "merge.guard", + previous: mergePrevious, + rotated: mergeRotated, + withRef: (candidate: TaskToPrProjection, ownerRef: TaskToPrRef) => ({ + ...candidate, + merge: { + guard: { + ...candidate.merge!.guard, + ref: ownerRef + } + } + }) + }, + { + name: "cleanup eligibility decision", + path: "cleanup.eligibility", + previous: cleanupPrevious, + rotated: cleanupRotated, + withRef: (candidate: TaskToPrProjection, ownerRef: TaskToPrRef) => ({ + ...candidate, + cleanup: { + eligibility: { + ...candidate.cleanup!.eligibility, + ref: ownerRef + } + } + }) + }, + { + name: "rollback target", + path: "rollback.plan", + previous: rollbackPrevious, + rotated: rollbackRotated, + withRef: (candidate: TaskToPrProjection, ownerRef: TaskToPrRef) => ({ + ...candidate, + rollback: { + plan: { + ...candidate.rollback!.plan, + ref: ownerRef + } + } + }) + } + ] as const; + + for (const ownerCase of cases) { + const retained = parseProjection({ + ...ownerCase.previous, + id: projectionId("valid-projection"), + }); + expect( + validateTaskToPrProjectionTransition(ownerCase.previous, retained), + `${ownerCase.name} permits an exactly retained owner record` + ).toEqual({ success: true, issues: [] }); + expect( + validateTaskToPrProjectionTransition(ownerCase.previous, ownerCase.rotated), + `${ownerCase.name} permits a fully fresh owner-record rotation` + ).toEqual({ success: true, issues: [] }); + + const previousRef = + ownerCase.path === "merge.guard" + ? ownerCase.previous.merge!.guard.ref + : ownerCase.path === "cleanup.eligibility" + ? ownerCase.previous.cleanup!.eligibility.ref + : ownerCase.previous.rollback!.plan.ref; + const rotatedRef = + ownerCase.path === "merge.guard" + ? ownerCase.rotated.merge!.guard.ref + : ownerCase.path === "cleanup.eligibility" + ? ownerCase.rotated.cleanup!.eligibility.ref + : ownerCase.rotated.rollback!.plan.ref; + const invalidRefs = [ + previousRef, + { + ...rotatedRef, + digest: previousRef.digest + }, + { + ...rotatedRef, + id: previousRef.id, + digest: sha("c") + } + ]; + + for (const [index, invalidRef] of invalidRefs.entries()) { + const invalid = ownerCase.withRef( + ownerCase.rotated, + invalidRef + ); + const parsedInvalid = TaskToPrProjectionSchema.safeParse(invalid); + if (!parsedInvalid.success) { + expect(parsedInvalid.success, `${ownerCase.name} stale provenance is rejected statically`).toBe(false); + continue; + } + const result = validateTaskToPrProjectionTransition(ownerCase.previous, invalid); + expect(result.success, `${ownerCase.name} invalid ref rotation ${index}`).toBe(false); + expect( + result.issues.some((issue) => issue.path === ownerCase.path), + `${ownerCase.name} reports its owner-record path` + ).toBe(true); + } + } + }); + + test("binds recovery and handoff transitions to the prior attempt, generation, WorkRun, and fresh attempt-scoped refs", () => { + const base = validProjection(); + const previous = parseProjection({ + ...base, + id: projectionId("valid-projection"), + createdAt: "2026-07-23T15:08:00.000Z", + state: "running", + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + merge: undefined, + recovery: undefined, + cleanup: undefined, + repair: { + ref: ref("repair_cycle", "todos", "0"), + cycle: 0, + cap: 2, + exhausted: false + }, + provenanceLedger: [], + evidenceRefs: [] + }); + const nextAttempt = { + ...previous.attempt, + ref: ref("attempt", "todos", "a"), + nonce: attemptNonce("next"), + admissionRef: ref("admission", "codewith", "next", "partial"), + admissionWriterGenerationRef: ref("writer_generation", "todos", "5"), + workerRef: ref("worker", "codewith", "next", "partial"), + runtimeRef: ref("runtime", "codewith", "b", "partial"), + writerGenerationRef: ref("writer_generation", "todos", "5"), + writerLeaseRef: ref("writer_lease", "repos", "d", "partial"), + writerFenceRef: ref("writer_fence", "repos", "e", "full"), + providerProfileRef: ref("provider_profile", "codewith", "f", "full"), + providerRouteRef: ref("provider_route", "codewith", "0", "partial") + }; + const nextWorkRunRef = ref("work_run", "codewith", "1"); + const recovery = { + ref: ref("recovery", "todos", "2"), + priorAttemptRef: previous.attempt.ref, + priorWriterGenerationRef: previous.attempt.writerGenerationRef, + priorWorkRunRef: previous.workRunRef, + successorAttemptNonce: nextAttempt.nonce, + successorWriterGenerationRef: nextAttempt.writerGenerationRef, + preservedStateRefs: [ + previous.workRunRef, + previous.rootRequestRef, + previous.prGroupRef, + previous.leafTaskRef, + previous.repository.repoRef, + previous.repository.worktreeRef, + previous.repository.branchRef, + previous.events.streamRef + ], + stopEvidenceRef: evidence("ev_transition_stop", "2"), + leaseRevocationEvidenceRef: evidence("ev_transition_revoke", "3") + }; + const recovered = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...previous, + id: projectionId("valid-projection"), + createdAt, + state: "recovering", + workRunRef: nextWorkRunRef, + attempt: nextAttempt, + recovery, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "4"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("5") + } + })); + expect(validateTaskToPrProjectionTransition(previous, recovered)).toEqual({ success: true, issues: [] }); + + const reusedAttemptDigest: TaskToPrProjection = { + ...recovered, + id: projectionId("valid-projection"), + attempt: { + ...recovered.attempt, + ref: { + ...recovered.attempt.ref, + id: "attempt:opaque-fresh-id-stale-digest", + digest: previous.attempt.ref.digest + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, reusedAttemptDigest).success).toBe(false); + + const reusedGenerationDigest: TaskToPrProjection = { + ...recovered, + id: projectionId("valid-projection"), + attempt: { + ...recovered.attempt, + writerGenerationRef: { + ...recovered.attempt.writerGenerationRef, + id: "writer_generation:opaque-fresh-id-stale-digest", + digest: previous.attempt.writerGenerationRef.digest + } + }, + recovery: { + ...recovered.recovery!, + successorWriterGenerationRef: { + ...recovered.attempt.writerGenerationRef, + id: "writer_generation:opaque-fresh-id-stale-digest", + digest: previous.attempt.writerGenerationRef.digest + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, reusedGenerationDigest).success).toBe(false); + + const reusedWorkRunDigest: TaskToPrProjection = { + ...recovered, + id: projectionId("valid-projection"), + workRunRef: { + ...recovered.workRunRef, + id: "work_run:opaque-fresh-id-stale-digest", + digest: previous.workRunRef.digest + } + }; + expect(validateTaskToPrProjectionTransition(previous, reusedWorkRunDigest).success).toBe(false); + + expect( + validateTaskToPrProjectionTransition(previous, { + ...recovered, + id: projectionId("valid-projection"), + attempt: { + ...recovered.attempt, + ref: { + ...recovered.attempt.ref, + id: previous.attempt.ref.id, + digest: sha("6") + } + } + }).success + ).toBe(false); + expect( + validateTaskToPrProjectionTransition(previous, { + ...recovered, + id: projectionId("valid-projection"), + attempt: { + ...recovered.attempt, + writerGenerationRef: { + ...recovered.attempt.writerGenerationRef, + id: previous.attempt.writerGenerationRef.id, + digest: sha("7") + } + }, + recovery: { + ...recovered.recovery, + successorWriterGenerationRef: { + ...recovered.attempt.writerGenerationRef, + id: previous.attempt.writerGenerationRef.id, + digest: sha("7") + } + } + }).success + ).toBe(false); + expect( + validateTaskToPrProjectionTransition(previous, { + ...recovered, + id: projectionId("valid-projection"), + workRunRef: { + ...recovered.workRunRef, + id: previous.workRunRef.id, + digest: sha("8") + } + }).success + ).toBe(false); + + for (const [field, staleRef] of [ + ["runtimeRef", previous.attempt.runtimeRef], + ["writerLeaseRef", previous.attempt.writerLeaseRef], + ["writerFenceRef", previous.attempt.writerFenceRef], + ["providerProfileRef", previous.attempt.providerProfileRef], + ["providerRouteRef", previous.attempt.providerRouteRef] + ] as const) { + expect( + validateTaskToPrProjectionTransition(previous, { + ...recovered, + id: projectionId("valid-projection"), + attempt: { ...recovered.attempt, [field]: staleRef } + }).success + ).toBe(false); + } + expect( + validateTaskToPrProjectionTransition(previous, { + ...recovered, + id: projectionId("valid-projection"), + attempt: { + ...recovered.attempt, + runtimeRef: { + ...recovered.attempt.runtimeRef, + id: "runtime:opaque-fresh-display", + digest: previous.attempt.runtimeRef.digest + } + } + }).success + ).toBe(false); + + expect( + validateTaskToPrProjectionTransition(previous, { + ...recovered, + id: projectionId("valid-projection"), + recovery: { ...recovered.recovery, priorWorkRunRef: nextWorkRunRef } + }).success + ).toBe(false); + + const handedOff = withActiveProvenance({ + ...recovered, + id: projectionId("valid-projection"), + state: "handed_off", + provenanceLedger: previous.provenanceLedger, + recovery: undefined, + handoff: { + ref: ref("handoff", "todos", "6"), + previousAttemptRef: previous.attempt.ref, + nextAttemptRef: nextAttempt.ref, + previousWriterGenerationRef: previous.attempt.writerGenerationRef, + nextWriterGenerationRef: nextAttempt.writerGenerationRef, + stoppedWorkRunRef: previous.workRunRef, + stopEvidenceRef: evidence("ev_handoff_stop", "6"), + leaseRevocationEvidenceRef: evidence("ev_handoff_revoke", "7") + } + }); + expect(validateTaskToPrProjectionTransition(previous, handedOff).success).toBe(true); + expect( + validateTaskToPrProjectionTransition(previous, { + ...handedOff, + id: projectionId("valid-projection"), + handoff: { + ...handedOff.handoff, + previousAttemptRef: nextAttempt.ref + } + }).success + ).toBe(false); + + const retainedHandoff = withActiveProvenance({ + ...handedOff, + id: projectionId("valid-projection"), + events: { + ...handedOff.events, + replayCursorRef: ref("replay_cursor", "todos", "8"), + sequence: handedOff.events.sequence + 1, + prefixDigest: sha("9") + } + }); + expect(validateTaskToPrProjectionTransition(handedOff, retainedHandoff)).toEqual({ + success: true, + issues: [] + }); + + const removedHandoff = withActiveProvenance({ + ...retainedHandoff, + id: projectionId("valid-projection"), + state: "running" as const, + handoff: undefined + }); + expect(TaskToPrProjectionSchema.safeParse(removedHandoff).success).toBe(true); + expect(validateTaskToPrProjectionTransition(handedOff, removedHandoff).success).toBe(false); + + const mutatedHandoff = withActiveProvenance({ + ...retainedHandoff, + id: projectionId("valid-projection"), + handoff: { + ...retainedHandoff.handoff, + stopEvidenceRef: evidence("ev_handoff_stop_mutated", "a") + } + }); + expect(TaskToPrProjectionSchema.safeParse(mutatedHandoff).success).toBe(true); + expect(validateTaskToPrProjectionTransition(handedOff, mutatedHandoff).success).toBe(false); + + const switchedToRecovery = withActiveProvenance({ + ...retainedHandoff, + id: projectionId("valid-projection"), + state: "recovering" as const, + handoff: undefined, + recovery: { + ...recovery, + successorAttemptNonce: handedOff.attempt.nonce, + successorWriterGenerationRef: handedOff.attempt.writerGenerationRef + } + }); + expect(TaskToPrProjectionSchema.safeParse(switchedToRecovery).success).toBe(true); + expect(validateTaskToPrProjectionTransition(handedOff, switchedToRecovery).success).toBe(false); + + const retainedRecovery = withActiveProvenance({ + ...recovered, + id: projectionId("valid-projection"), + events: { + ...recovered.events, + replayCursorRef: ref("replay_cursor", "todos", "b"), + sequence: recovered.events.sequence + 1, + prefixDigest: sha("c") + } + }); + expect(validateTaskToPrProjectionTransition(recovered, retainedRecovery)).toEqual({ + success: true, + issues: [] + }); + const mutatedRecovery = withActiveProvenance({ + ...retainedRecovery, + id: projectionId("valid-projection"), + recovery: { + ...retainedRecovery.recovery!, + stopEvidenceRef: evidence("ev_recovery_stop_mutated", "d") + } + }); + expect(TaskToPrProjectionSchema.safeParse(mutatedRecovery).success).toBe(true); + expect(validateTaskToPrProjectionTransition(recovered, mutatedRecovery).success).toBe(false); + }); + + test("requires fresh handoff and recovery owner refs across consecutive attempt and WorkRun rotations", () => { + const projection = validProjection(); + const initial = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "running", + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + merge: undefined, + recovery: undefined, + cleanup: undefined, + rollback: undefined, + repair: { + ref: ref("repair_cycle", "todos", "0"), + cycle: 0, + cap: 2, + exhausted: false + }, + provenanceLedger: [], + evidenceRefs: [] + }); + const firstSeeds = { + attempt: "a", + runtime: "b", + generation: "5", + lease: "d", + fence: "e", + profile: "f", + route: "0", + workRun: "1", + cursor: "2", + prefix: "3", + owner: "4", + stop: "5", + revoke: "6" + } as const; + const secondSeeds = { + attempt: "b", + runtime: "c", + generation: "6", + lease: "e", + fence: "f", + profile: "0", + route: "1", + workRun: "2", + cursor: "3", + prefix: "4", + owner: "5", + stop: "6", + revoke: "7" + } as const; + + function rotateAttemptOwner( + previous: TaskToPrProjection, + owner: "handoff" | "recovery", + seeds: typeof firstSeeds | typeof secondSeeds, + label: string + ): TaskToPrProjection { + const nextAttempt = { + ...previous.attempt, + ref: ref("attempt", "todos", seeds.attempt), + nonce: attemptNonce(`${label}`), + admissionRef: ref("admission", "codewith", `${label}-admission`, "partial"), + admissionWriterGenerationRef: ref("writer_generation", "todos", seeds.generation), + workerRef: ref("worker", "codewith", `${label}-worker`, "partial"), + runtimeRef: ref("runtime", "codewith", seeds.runtime, "partial"), + writerGenerationRef: ref("writer_generation", "todos", seeds.generation), + writerLeaseRef: ref("writer_lease", "repos", seeds.lease, "partial"), + writerFenceRef: ref("writer_fence", "repos", seeds.fence, "full"), + providerProfileRef: ref("provider_profile", "codewith", seeds.profile, "full"), + providerRouteRef: ref("provider_route", "codewith", seeds.route, "partial") + }; + const nextWorkRunRef = ref("work_run", "codewith", seeds.workRun); + const transitionRef = ref(owner, "todos", seeds.owner); + const preservedStateRefs = [ + previous.workRunRef, + previous.rootRequestRef, + previous.prGroupRef, + previous.leafTaskRef, + previous.repository.repoRef, + previous.repository.worktreeRef, + previous.repository.branchRef, + previous.events.streamRef + ]; + + return TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...previous, + id: projectionId("valid-projection"), + state: owner === "handoff" ? "handed_off" : "recovering", + workRunRef: nextWorkRunRef, + attempt: nextAttempt, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", seeds.cursor), + sequence: previous.events.sequence + 1, + prefixDigest: sha(seeds.prefix) + }, + handoff: + owner === "handoff" + ? { + ref: transitionRef, + previousAttemptRef: previous.attempt.ref, + nextAttemptRef: nextAttempt.ref, + previousWriterGenerationRef: previous.attempt.writerGenerationRef, + nextWriterGenerationRef: nextAttempt.writerGenerationRef, + stoppedWorkRunRef: previous.workRunRef, + stopEvidenceRef: evidence(`ev_${owner}_${label}_stop`, seeds.stop), + leaseRevocationEvidenceRef: evidence(`ev_${owner}_${label}_revoke`, seeds.revoke) + } + : undefined, + recovery: + owner === "recovery" + ? { + ref: transitionRef, + priorAttemptRef: previous.attempt.ref, + priorWriterGenerationRef: previous.attempt.writerGenerationRef, + priorWorkRunRef: previous.workRunRef, + successorAttemptNonce: nextAttempt.nonce, + successorWriterGenerationRef: nextAttempt.writerGenerationRef, + preservedStateRefs, + stopEvidenceRef: evidence(`ev_${owner}_${label}_stop`, seeds.stop), + leaseRevocationEvidenceRef: evidence(`ev_${owner}_${label}_revoke`, seeds.revoke) + } + : undefined + })); + } + + for (const owner of ["handoff", "recovery"] as const) { + const first = rotateAttemptOwner(initial, owner, firstSeeds, "first"); + expect(validateTaskToPrProjectionTransition(initial, first), `${owner} establishes the first rotated owner fact`).toEqual({ + success: true, + issues: [] + }); + const rotated = rotateAttemptOwner(first, owner, secondSeeds, "second"); + expect( + validateTaskToPrProjectionTransition(first, rotated), + `${owner} permits a fully fresh owner-record rotation` + ).toEqual({ success: true, issues: [] }); + + const previousRef = owner === "handoff" ? first.handoff!.ref : first.recovery!.ref; + const rotatedRef = owner === "handoff" ? rotated.handoff!.ref : rotated.recovery!.ref; + const invalidRefs = [ + previousRef, + { + ...rotatedRef, + digest: previousRef.digest + }, + { + ...rotatedRef, + id: previousRef.id, + digest: sha("8") + } + ]; + + for (const [index, invalidRef] of invalidRefs.entries()) { + const invalid = { + ...rotated, + [owner]: { + ...(owner === "handoff" ? rotated.handoff! : rotated.recovery!), + ref: invalidRef + } + }; + const parsedInvalid = TaskToPrProjectionSchema.safeParse(invalid); + if (!parsedInvalid.success) { + expect(parsedInvalid.success, `${owner} stale provenance is rejected statically`).toBe(false); + continue; + } + const result = validateTaskToPrProjectionTransition(first, invalid); + expect(result.success, `${owner} invalid ref rotation ${index}`).toBe(false); + expect(result.issues.some((issue) => issue.path === owner), `${owner} reports its owner-record path`).toBe( + true + ); + } + } + }); + + test("rejects A-to-C reuse across every attempt, replay, and repair provenance identity", () => { + const projection = validProjection(); + const initial = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "running", + workRunRef: ref("work_run", "codewith", "lineage-a"), + attempt: { + ...projection.attempt, + ref: ref("attempt", "todos", "lineage-a"), + nonce: attemptNonce("lineage-a"), + runtimeRef: ref("runtime", "codewith", "lineage-a", "partial"), + writerGenerationRef: ref("writer_generation", "todos", "lineage-a"), + writerLeaseRef: ref("writer_lease", "repos", "lineage-a", "partial"), + writerFenceRef: ref("writer_fence", "repos", "lineage-a", "full"), + providerProfileRef: ref("provider_profile", "codewith", "lineage-a", "full"), + providerRouteRef: ref("provider_route", "codewith", "lineage-a", "partial") + }, + events: { + ...projection.events, + replayCursorRef: ref("replay_cursor", "todos", "lineage-a"), + sequence: 0, + prefixDigest: sha("0") + }, + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + repair: { + ref: ref("repair_cycle", "todos", "lineage-a"), + cycle: 0, + cap: 2, + exhausted: false + }, + merge: undefined, + recovery: undefined, + cancellation: undefined, + cleanup: undefined, + rollback: undefined, + provenanceLedger: [], + evidenceRefs: [] + }); + + function rotateAttempt( + previous: TaskToPrProjection, + label: "b" | "c", + prefixDigest: string + ): TaskToPrProjection { + const nextAttempt = { + ...previous.attempt, + ref: ref("attempt", "todos", `lineage-${label}`), + nonce: attemptNonce(`lineage-${label}`), + admissionRef: ref("admission", "codewith", `lineage-${label}`, "partial"), + admissionWriterGenerationRef: ref("writer_generation", "todos", `lineage-${label}`), + workerRef: ref("worker", "codewith", `lineage-${label}`, "partial"), + runtimeRef: ref("runtime", "codewith", `lineage-${label}`, "partial"), + writerGenerationRef: ref("writer_generation", "todos", `lineage-${label}`), + writerLeaseRef: ref("writer_lease", "repos", `lineage-${label}`, "partial"), + writerFenceRef: ref("writer_fence", "repos", `lineage-${label}`, "full"), + providerProfileRef: ref("provider_profile", "codewith", `lineage-${label}`, "full"), + providerRouteRef: ref("provider_route", "codewith", `lineage-${label}`, "partial") + }; + return parseProjection({ + ...previous, + id: projectionId("valid-projection"), + state: "handed_off", + workRunRef: ref("work_run", "codewith", `lineage-${label}`), + attempt: nextAttempt, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", `lineage-${label}`), + sequence: previous.events.sequence + 1, + prefixDigest + }, + handoff: { + ref: ref("handoff", "todos", `lineage-${label}`), + previousAttemptRef: previous.attempt.ref, + nextAttemptRef: nextAttempt.ref, + previousWriterGenerationRef: previous.attempt.writerGenerationRef, + nextWriterGenerationRef: nextAttempt.writerGenerationRef, + stoppedWorkRunRef: previous.workRunRef, + stopEvidenceRef: evidence(`lineage_${label}_stop`, label === "b" ? "1" : "2"), + leaseRevocationEvidenceRef: evidence( + `lineage_${label}_revoke`, + label === "b" ? "3" : "4" + ) + }, + recovery: undefined, + provenanceLedger: previous.provenanceLedger + }); + } + + const middle = rotateAttempt(initial, "b", sha("1")); + const current = rotateAttempt(middle, "c", sha("2")); + expect(validateTaskToPrProjectionTransition(initial, middle)).toEqual({ + success: true, + issues: [] + }); + expect(validateTaskToPrProjectionTransition(middle, current)).toEqual({ + success: true, + issues: [] + }); + + expect(initial.id).toBe(middle.id); + expect(middle.id).toBe(current.id); + + type RefReuseCase = { + category: + | "work_run" + | "attempt" + | "runtime" + | "writer_generation" + | "writer_lease" + | "writer_fence" + | "provider_profile" + | "provider_route" + | "replay_cursor"; + prior: TaskToPrRef; + read: (value: TaskToPrProjection) => TaskToPrRef; + write: (value: TaskToPrProjection, next: TaskToPrRef) => void; + }; + const refReuseCases: readonly RefReuseCase[] = [ + { + category: "work_run", + prior: initial.workRunRef, + read: (value) => value.workRunRef, + write: (value, next) => { + value.workRunRef = next; + } + }, + { + category: "attempt", + prior: initial.attempt.ref, + read: (value) => value.attempt.ref, + write: (value, next) => { + value.attempt.ref = next; + value.handoff!.nextAttemptRef = next; + } + }, + { + category: "runtime", + prior: initial.attempt.runtimeRef, + read: (value) => value.attempt.runtimeRef, + write: (value, next) => { + value.attempt.runtimeRef = next; + } + }, + { + category: "writer_generation", + prior: initial.attempt.writerGenerationRef, + read: (value) => value.attempt.writerGenerationRef, + write: (value, next) => { + value.attempt.writerGenerationRef = next; + value.handoff!.nextWriterGenerationRef = next; + } + }, + { + category: "writer_lease", + prior: initial.attempt.writerLeaseRef, + read: (value) => value.attempt.writerLeaseRef, + write: (value, next) => { + value.attempt.writerLeaseRef = next; + } + }, + { + category: "writer_fence", + prior: initial.attempt.writerFenceRef, + read: (value) => value.attempt.writerFenceRef, + write: (value, next) => { + value.attempt.writerFenceRef = next; + } + }, + { + category: "provider_profile", + prior: initial.attempt.providerProfileRef, + read: (value) => value.attempt.providerProfileRef, + write: (value, next) => { + value.attempt.providerProfileRef = next; + } + }, + { + category: "provider_route", + prior: initial.attempt.providerRouteRef, + read: (value) => value.attempt.providerRouteRef, + write: (value, next) => { + value.attempt.providerRouteRef = next; + } + }, + { + category: "replay_cursor", + prior: initial.events.replayCursorRef, + read: (value) => value.events.replayCursorRef, + write: (value, next) => { + value.events.replayCursorRef = next; + } + } + ]; + + function latestCategoryIndex( + value: TaskToPrProjection, + category: TaskToPrProjection["provenanceLedger"][number]["category"] + ): number { + for (let index = value.provenanceLedger.length - 1; index >= 0; index -= 1) { + if (value.provenanceLedger[index]!.category === category) { + return index; + } + } + throw new Error(`missing ${category} provenance entry`); + } + + for (const reuseCase of refReuseCases) { + const identityReuse = structuredClone(current); + reuseCase.write(identityReuse, reuseCase.prior); + identityReuse.provenanceLedger.splice( + latestCategoryIndex(identityReuse, reuseCase.category), + 1 + ); + expect(TaskToPrProjectionSchema.safeParse(identityReuse).success, `${reuseCase.category} id fixture`).toBe( + true + ); + expect( + validateTaskToPrProjectionTransition(middle, identityReuse).success, + `${reuseCase.category} A identity cannot reactivate in C` + ).toBe(false); + + const digestReuse = structuredClone(current); + const currentRef = reuseCase.read(digestReuse); + const reusedDigestRef = { ...currentRef, digest: reuseCase.prior.digest }; + reuseCase.write(digestReuse, reusedDigestRef); + digestReuse.provenanceLedger[latestCategoryIndex(digestReuse, reuseCase.category)] = { + category: reuseCase.category, + ref: reusedDigestRef + }; + expect( + TaskToPrProjectionSchema.safeParse(digestReuse).success, + `${reuseCase.category} A digest cannot be reused in C` + ).toBe(false); + } + + const nonceReuse = structuredClone(current); + nonceReuse.attempt.nonce = initial.attempt.nonce; + nonceReuse.provenanceLedger.splice(latestCategoryIndex(nonceReuse, "attempt_nonce"), 1); + expect(TaskToPrProjectionSchema.safeParse(nonceReuse).success).toBe(true); + expect(validateTaskToPrProjectionTransition(middle, nonceReuse).success).toBe(false); + + const duplicateNonceTombstone = structuredClone(current); + duplicateNonceTombstone.attempt.nonce = initial.attempt.nonce; + duplicateNonceTombstone.provenanceLedger[ + latestCategoryIndex(duplicateNonceTombstone, "attempt_nonce") + ] = { + category: "attempt_nonce", + nonce: initial.attempt.nonce + }; + expect(TaskToPrProjectionSchema.safeParse(duplicateNonceTombstone).success).toBe(false); + + const prefixReuse = structuredClone(current); + prefixReuse.events.prefixDigest = initial.events.prefixDigest; + prefixReuse.provenanceLedger[latestCategoryIndex(prefixReuse, "replay_prefix")] = { + category: "replay_prefix", + sequence: prefixReuse.events.sequence, + prefixDigest: initial.events.prefixDigest + }; + expect(TaskToPrProjectionSchema.safeParse(prefixReuse).success).toBe(false); + + const repairMiddle = parseProjection({ + ...initial, + id: projectionId("valid-projection"), + state: "repairing", + events: { + ...initial.events, + replayCursorRef: ref("replay_cursor", "todos", "repair-b"), + sequence: initial.events.sequence + 1, + prefixDigest: sha("3") + }, + repair: { + ref: ref("repair_cycle", "todos", "repair-b-state"), + cycle: 1, + cap: 2, + exhausted: false, + latestRepairRef: ref("repair_cycle", "todos", "repair-b-latest") + }, + provenanceLedger: initial.provenanceLedger + }); + const repairCurrent = parseProjection({ + ...repairMiddle, + id: projectionId("valid-projection"), + events: { + ...repairMiddle.events, + replayCursorRef: ref("replay_cursor", "todos", "repair-c"), + sequence: repairMiddle.events.sequence + 1, + prefixDigest: sha("4") + }, + repair: { + ref: ref("repair_cycle", "todos", "repair-c-state"), + cycle: 2, + cap: 2, + exhausted: true, + latestRepairRef: ref("repair_cycle", "todos", "repair-c-latest") + }, + provenanceLedger: repairMiddle.provenanceLedger + }); + expect(validateTaskToPrProjectionTransition(initial, repairMiddle)).toEqual({ + success: true, + issues: [] + }); + expect(validateTaskToPrProjectionTransition(repairMiddle, repairCurrent)).toEqual({ + success: true, + issues: [] + }); + + const repairStateIdentityReuse = structuredClone(repairCurrent); + repairStateIdentityReuse.repair.ref = initial.repair.ref; + repairStateIdentityReuse.provenanceLedger.splice( + latestCategoryIndex(repairStateIdentityReuse, "repair_state"), + 1 + ); + expect(TaskToPrProjectionSchema.safeParse(repairStateIdentityReuse).success).toBe(true); + expect( + validateTaskToPrProjectionTransition(repairMiddle, repairStateIdentityReuse).success + ).toBe(false); + + for (const category of ["repair_state", "latest_repair"] as const) { + const repairDigestReuse = structuredClone(repairCurrent); + const reused = { + ...(category === "repair_state" + ? repairDigestReuse.repair.ref + : repairDigestReuse.repair.latestRepairRef!), + digest: initial.repair.ref.digest + }; + if (category === "repair_state") { + repairDigestReuse.repair.ref = reused; + } else { + repairDigestReuse.repair.latestRepairRef = reused; + } + repairDigestReuse.provenanceLedger[latestCategoryIndex(repairDigestReuse, category)] = { + category, + ref: reused + }; + expect( + TaskToPrProjectionSchema.safeParse(repairDigestReuse).success, + `${category} cannot reuse the A repair digest in cycle 2` + ).toBe(false); + } + + const latestRepairIdentityReuse = structuredClone(repairCurrent); + latestRepairIdentityReuse.repair.latestRepairRef = initial.repair.ref; + latestRepairIdentityReuse.provenanceLedger[ + latestCategoryIndex(latestRepairIdentityReuse, "latest_repair") + ] = { + category: "latest_repair", + ref: initial.repair.ref + }; + expect(TaskToPrProjectionSchema.safeParse(latestRepairIdentityReuse).success).toBe(false); + }); + + test("requires fresh repair refs and enforces the legal state graph without active re-entry", () => { + const base = validProjection(); + const previous = parseProjection({ + ...base, + id: projectionId("valid-projection"), + createdAt: "2026-07-23T15:09:00.000Z", + state: "reviewing", + cleanup: undefined, + merge: undefined, + repair: { + ref: ref("repair_cycle", "todos", "0"), + cycle: 1, + cap: 2, + exhausted: false, + latestRepairRef: ref("repair_cycle", "todos", "5") + } + }); + const repairing = parseProjection({ + ...previous, + id: projectionId("valid-projection"), + createdAt, + state: "repairing", + repair: { + ref: ref("repair_cycle", "todos", "1"), + cycle: 2, + cap: 2, + exhausted: true, + latestRepairRef: ref("repair_cycle", "todos", "2") + }, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "3"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("4") + } + }); + expect(validateTaskToPrProjectionTransition(previous, repairing).success).toBe(true); + + const reusedRepairStateDigest: TaskToPrProjection = { + ...repairing, + id: projectionId("valid-projection"), + repair: { + ...repairing.repair, + ref: { + ...repairing.repair.ref, + id: "repair_cycle:opaque-fresh-state-id-stale-digest", + digest: previous.repair.ref.digest + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, reusedRepairStateDigest).success).toBe(false); + + const reusedLatestRepairDigest: TaskToPrProjection = { + ...repairing, + id: projectionId("valid-projection"), + repair: { + ...repairing.repair, + latestRepairRef: { + ...repairing.repair.latestRepairRef!, + id: "repair_cycle:opaque-fresh-latest-id-stale-digest", + digest: previous.repair.latestRepairRef!.digest + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, reusedLatestRepairDigest).success).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...repairing, + repair: { + ...repairing.repair, + latestRepairRef: { + ...repairing.repair.latestRepairRef!, + digest: repairing.repair.ref.digest + } + } + }).success + ).toBe(false); + + const repairStateReusingPriorLatest: TaskToPrProjection = { + ...repairing, + id: projectionId("valid-projection"), + repair: { + ...repairing.repair, + ref: { + ...repairing.repair.ref, + digest: previous.repair.latestRepairRef!.digest + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, repairStateReusingPriorLatest).success).toBe(false); + + const latestRepairReusingPriorState: TaskToPrProjection = { + ...repairing, + id: projectionId("valid-projection"), + repair: { + ...repairing.repair, + latestRepairRef: { + ...repairing.repair.latestRepairRef!, + digest: previous.repair.ref.digest + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, latestRepairReusingPriorState).success).toBe(false); + + expect( + validateTaskToPrProjectionTransition(previous, { + ...repairing, + id: projectionId("valid-projection"), + repair: { + ...repairing.repair, + ref: previous.repair.ref + } + }).success + ).toBe(false); + + expect( + validateTaskToPrProjectionTransition( + parseProjection({ + ...previous, + id: projectionId("valid-projection"), + state: "running", + reviews: [], + exactHead: undefined, + pullRequestRef: undefined + }), + { + ...previous, + id: projectionId("valid-projection"), + state: "admitted", + reviews: [], + exactHead: undefined, + pullRequestRef: undefined + } + ).success + ).toBe(false); + expect( + validateTaskToPrProjectionTransition(base, { + ...base, + id: projectionId("valid-projection"), + state: "running", + merge: undefined, + exactHead: undefined, + reviews: [], + pullRequestRef: undefined, + cleanup: undefined + }).success + ).toBe(false); + }); + + test("enforces bidirectional lifecycle phase requirements", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "handed_off", + handoff: undefined, + merge: undefined, + cleanup: undefined + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "reviewing", + reviews: [], + merge: undefined, + cleanup: undefined + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + cleanup: { + ...projection.cleanup, + outcome: { ...projection.cleanup.outcome, status: "failed" } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "running" + }).success + ).toBe(false); + + const reviewOnly = parseProjection({ + ...projection, + state: "reviewing", + merge: undefined, + cleanup: undefined + }); + for (const state of ["admitted", "running", "handed_off"] as const) { + expect( + TaskToPrProjectionSchema.safeParse({ + ...reviewOnly, + state, + handoff: state === "handed_off" ? projection.handoff : undefined, + recovery: undefined + }).success + ).toBe(false); + } + + const earlyEligibleGuard = { + ...projection, + state: "reviewing" as const, + merge: { + guard: projection.merge.guard + }, + cleanup: undefined + }; + expect(TaskToPrProjectionSchema.safeParse(earlyEligibleGuard).success).toBe(false); + + const runningPrevious = parseProjection({ + ...reviewOnly, + id: projectionId("valid-projection"), + state: "running", + reviews: [] + }); + const runningWithApprovedReview: TaskToPrProjection = { + ...runningPrevious, + id: projectionId("valid-projection"), + reviews: reviewOnly.reviews + }; + expect(TaskToPrProjectionSchema.safeParse(runningWithApprovedReview).success).toBe(false); + expect(validateTaskToPrProjectionTransition(runningPrevious, runningWithApprovedReview).success).toBe(false); + }); + + test("requires replay advancement for semantic lifecycle drift and preserves canonical pull-request identity", () => { + const projection = validProjection(); + const runningWithPullRequest = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "running", + exactHead: undefined, + reviews: [], + merge: undefined, + cleanup: undefined + }); + + const phaseDrift = withActiveProvenance({ + ...runningWithPullRequest, + id: projectionId("valid-projection"), + state: "blocked", + terminalDispositionRef: ref("terminal_disposition", "todos", "blocked-stale-cursor"), + evidenceRefs: [evidence("blocked_stale_cursor", "1")] + }); + const phaseDriftResult = validateTaskToPrProjectionTransition(runningWithPullRequest, phaseDrift); + expect(phaseDriftResult.success).toBe(false); + expect(phaseDriftResult.issues.some((issue) => issue.path === "events.sequence")).toBe(true); + + const recoveryProjection = parseProjection({ + ...validRecoveryProjection(), + id: projectionId("valid-projection"), + state: "recovering", + pullRequestRef: undefined, + exactHead: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + recovery: { + ...validRecoveryProjection().recovery, + preservedStateRefs: validRecoveryProjection().recovery.preservedStateRefs.filter( + (preservedRef) => preservedRef.role !== "pull_request" + ) + } + }); + const recoveryIdentityDrift = withActiveProvenance({ + ...recoveryProjection, + id: projectionId("valid-projection"), + recovery: { + ...recoveryProjection.recovery!, + ref: ref("recovery", "todos", "a") + } + }); + const recoveryDriftResult = validateTaskToPrProjectionTransition(recoveryProjection, recoveryIdentityDrift); + expect(recoveryDriftResult.success).toBe(false); + expect(recoveryDriftResult.issues.some((issue) => issue.path === "events.sequence")).toBe(true); + + const advancedEvents = { + ...runningWithPullRequest.events, + replayCursorRef: ref("replay_cursor", "todos", "a"), + sequence: runningWithPullRequest.events.sequence + 1, + prefixDigest: sha("b") + }; + for (const pullRequestRef of [undefined, ref("pull_request", "todos", "c")]) { + const pullRequestDriftResult = validateTaskToPrProjectionTransition( + runningWithPullRequest, + withActiveProvenance({ + ...runningWithPullRequest, + id: projectionId("valid-projection"), + events: advancedEvents, + pullRequestRef + }) + ); + expect(pullRequestDriftResult.success).toBe(false); + expect(pullRequestDriftResult.issues.some((issue) => issue.path === "pullRequestRef")).toBe(true); + } + }); + + test("preserves immutable same-head review history and permits append-only review evidence", () => { + const projection = validProjection(); + const adverseReview = { + ...projection.reviews[0], + verdict: "changes_requested" as const + }; + const previous = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...projection, + id: projectionId("valid-projection"), + state: "reviewing", + reviews: [adverseReview], + merge: undefined, + cleanup: undefined + })); + const approvedReview = { + ref: ref("review", "review", "3"), + pullRequestRef: projection.pullRequestRef, + base: projection.repository.baseHead, + head: projection.repository.branchHead, + reviewerRef: ref("reviewer", "review", "3"), + reviewRunRef: ref("review_run", "review", "4"), + proofBundleRef: ref("proof_bundle", "review", "5"), + verdict: "approved" as const, + reviewedAt: createdAt + }; + const advancedEvents = { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "6"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("7") + }; + + const appendOnly = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...previous, + id: projectionId("valid-projection"), + events: advancedEvents, + reviews: [adverseReview, approvedReview] + })); + expect(validateTaskToPrProjectionTransition(previous, appendOnly)).toEqual({ success: true, issues: [] }); + + const reorderedHistory = parseProjection({ + ...appendOnly, + id: projectionId("valid-projection"), + events: { + ...appendOnly.events, + replayCursorRef: ref("replay_cursor", "todos", "8"), + sequence: appendOnly.events.sequence + 1, + prefixDigest: sha("9") + }, + reviews: [approvedReview, adverseReview] + }); + expect(validateTaskToPrProjectionTransition(appendOnly, reorderedHistory).success).toBe(false); + + const prependedHistory = parseProjection({ + ...appendOnly, + id: projectionId("valid-projection"), + reviews: [approvedReview, adverseReview] + }); + expect(validateTaskToPrProjectionTransition(previous, prependedHistory).success).toBe(false); + + const rewrittenAdverseReview = validateTaskToPrProjectionTransition( + previous, + withActiveProvenance({ + ...previous, + id: projectionId("valid-projection"), + events: advancedEvents, + reviews: [{ ...adverseReview, verdict: "approved" }] + }) + ); + expect(rewrittenAdverseReview.success).toBe(false); + expect(rewrittenAdverseReview.issues.some((issue) => issue.path === "reviews")).toBe(true); + + const replacedForEligibility = withActiveProvenance({ + ...appendOnly, + id: projectionId("valid-projection"), + state: "merge_ready" as const, + reviews: [approvedReview], + merge: { + guard: { + ...projection.merge.guard, + reviewRefs: [approvedReview.ref], + proofBundleRefs: [ + approvedReview.proofBundleRef, + projection.exactHead.equalityProofRef, + ...projection.exactHead.ciProofBundleRefs + ] + } + } + }); + const replacedHistoryResult = validateTaskToPrProjectionTransition(previous, replacedForEligibility); + expect(replacedHistoryResult.success).toBe(false); + expect(replacedHistoryResult.issues.some((issue) => issue.path === "reviews")).toBe(true); + }); + + test("preserves the complete exact-head fact for an unchanged branch head", () => { + const projection = validProjection(); + const secondCiProof = ref("proof_bundle", "review", "3"); + const previous = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...projection, + id: projectionId("valid-projection"), + createdAt: "2026-07-23T15:09:00.000Z", + state: "running", + reviews: [], + merge: undefined, + cleanup: undefined, + exactHead: { + ...projection.exactHead, + ciProofBundleRefs: [...projection.exactHead.ciProofBundleRefs, secondCiProof] + } + })); + const advancedEvents = { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "3"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("4") + }; + const retained = parseProjection({ + ...previous, + id: projectionId("valid-projection"), + createdAt, + events: advancedEvents + }); + expect(validateTaskToPrProjectionTransition(previous, retained)).toEqual({ success: true, issues: [] }); + + const removed = parseProjection({ + ...retained, + id: projectionId("valid-projection"), + exactHead: undefined + }); + expect(validateTaskToPrProjectionTransition(previous, removed).success).toBe(false); + + const replacedEqualityProof = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...retained, + id: projectionId("valid-projection"), + exactHead: { + ...retained.exactHead!, + equalityProofRef: ref("proof_bundle", "review", "5") + } + })); + expect(validateTaskToPrProjectionTransition(previous, replacedEqualityProof).success).toBe(false); + + const reorderedCiProofs = parseProjection({ + ...retained, + id: projectionId("valid-projection"), + exactHead: { + ...retained.exactHead!, + ciProofBundleRefs: [...retained.exactHead!.ciProofBundleRefs].reverse() + } + }); + expect(validateTaskToPrProjectionTransition(previous, reorderedCiProofs).success).toBe(false); + }); + + test("requires fresh exact-head proof and review identities after branch-head changes", () => { + const projection = validProjection(); + const previous = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + createdAt: "2026-07-23T15:09:00.000Z", + state: "reviewing", + merge: undefined, + cleanup: undefined + }); + const nextHead = head("b"); + const freshCrossHead = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...previous, + id: projectionId("valid-projection"), + createdAt: "2026-07-23T15:12:00.000Z", + repository: { + ...previous.repository, + branchHead: nextHead + }, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "a"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("b") + }, + exactHead: { + ...previous.exactHead!, + localHead: nextHead, + remoteHead: nextHead, + providerPullRequestHead: nextHead, + equalityProofRef: ref("proof_bundle", "review", "3"), + ciProofBundleRefs: [ref("proof_bundle", "review", "4")], + verifiedAt: "2026-07-23T15:12:00.000Z" + }, + reviews: previous.reviews.map((review) => ({ + ...review, + ref: ref("review", "review", "7"), + head: nextHead, + reviewRunRef: ref("review_run", "review", "9"), + proofBundleRef: ref("proof_bundle", "review", "5"), + reviewedAt: "2026-07-23T15:12:00.000Z" + })) + })); + + expect(validateTaskToPrProjectionTransition(previous, freshCrossHead)).toEqual({ success: true, issues: [] }); + expect(freshCrossHead.reviews[0]!.reviewerRef).toEqual(previous.reviews[0]!.reviewerRef); + + const reusedEverything: TaskToPrProjection = { + ...freshCrossHead, + id: projectionId("valid-projection"), + exactHead: { + ...freshCrossHead.exactHead!, + equalityProofRef: previous.exactHead!.equalityProofRef, + ciProofBundleRefs: previous.exactHead!.ciProofBundleRefs + }, + reviews: previous.reviews.map((review) => ({ + ...review, + head: nextHead, + reviewedAt: "2026-07-23T15:12:00.000Z" + })) + }; + expect(TaskToPrProjectionSchema.safeParse(reusedEverything).success).toBe(false); + expect(validateTaskToPrProjectionTransition(previous, reusedEverything).success).toBe(false); + + const staleIdentityCases: Array<{ + label: string; + mutate: (candidate: TaskToPrProjection) => void; + }> = [ + { + label: "equality proof canonical id", + mutate: (candidate) => { + candidate.exactHead!.equalityProofRef.id = previous.exactHead!.equalityProofRef.id; + } + }, + { + label: "equality proof digest", + mutate: (candidate) => { + candidate.exactHead!.equalityProofRef.digest = previous.exactHead!.equalityProofRef.digest; + } + }, + { + label: "CI proof canonical id", + mutate: (candidate) => { + candidate.exactHead!.ciProofBundleRefs[0]!.id = previous.exactHead!.ciProofBundleRefs[0]!.id; + } + }, + { + label: "CI proof digest", + mutate: (candidate) => { + candidate.exactHead!.ciProofBundleRefs[0]!.digest = previous.exactHead!.ciProofBundleRefs[0]!.digest; + } + }, + { + label: "review canonical id", + mutate: (candidate) => { + candidate.reviews[0]!.ref.id = previous.reviews[0]!.ref.id; + } + }, + { + label: "review digest", + mutate: (candidate) => { + candidate.reviews[0]!.ref.digest = previous.reviews[0]!.ref.digest; + } + }, + { + label: "review-run canonical id", + mutate: (candidate) => { + candidate.reviews[0]!.reviewRunRef.id = previous.reviews[0]!.reviewRunRef.id; + } + }, + { + label: "review-run digest", + mutate: (candidate) => { + candidate.reviews[0]!.reviewRunRef.digest = previous.reviews[0]!.reviewRunRef.digest; + } + }, + { + label: "review proof canonical id", + mutate: (candidate) => { + candidate.reviews[0]!.proofBundleRef.id = previous.reviews[0]!.proofBundleRef.id; + } + }, + { + label: "review proof digest", + mutate: (candidate) => { + candidate.reviews[0]!.proofBundleRef.digest = previous.reviews[0]!.proofBundleRef.digest; + } + } + ]; + for (const { label, mutate } of staleIdentityCases) { + const candidate = structuredClone(freshCrossHead); + candidate.id = previous.id; + mutate(candidate); + expect(validateTaskToPrProjectionTransition(previous, candidate).success, label).toBe(false); + } + + const reorderedAndExpanded: TaskToPrProjection = { + ...freshCrossHead, + id: projectionId("valid-projection"), + exactHead: { + ...freshCrossHead.exactHead!, + ciProofBundleRefs: [ + ref("proof_bundle", "review", "8"), + previous.exactHead!.equalityProofRef + ] + } + }; + expect(TaskToPrProjectionSchema.safeParse(reorderedAndExpanded).success).toBe(false); + expect(validateTaskToPrProjectionTransition(previous, reorderedAndExpanded).success).toBe(false); + + type HeadBoundEvidenceCategory = { + name: string; + path: string; + read: (value: TaskToPrProjection) => TaskToPrRef; + }; + const headBoundEvidenceCategories: readonly HeadBoundEvidenceCategory[] = [ + { + name: "equality-proof", + path: "exactHead.equalityProofRef", + read: (value) => value.exactHead!.equalityProofRef + }, + { + name: "ci-proof", + path: "exactHead.ciProofBundleRefs.0", + read: (value) => value.exactHead!.ciProofBundleRefs[0]! + }, + { + name: "review-proof", + path: "reviews.0.proofBundleRef", + read: (value) => value.reviews[0]!.proofBundleRef + }, + { + name: "review-record", + path: "reviews.0.ref", + read: (value) => value.reviews[0]!.ref + }, + { + name: "review-run", + path: "reviews.0.reviewRunRef", + read: (value) => value.reviews[0]!.reviewRunRef + } + ]; + const crossHeadCategoryPairs = headBoundEvidenceCategories.flatMap((priorCategory) => + headBoundEvidenceCategories.map((currentCategory) => ({ priorCategory, currentCategory })) + ); + expect(crossHeadCategoryPairs).toHaveLength(25); + + for (const { priorCategory, currentCategory } of crossHeadCategoryPairs) { + const caseLabel = `${priorCategory.name} to ${currentCategory.name}`; + + const reusedIdentity = structuredClone(freshCrossHead); + currentCategory.read(reusedIdentity).id = priorCategory.read(previous).id; + const identitySchemaResult = TaskToPrProjectionSchema.safeParse(reusedIdentity); + const identityTransitionResult = validateTaskToPrProjectionTransition(previous, reusedIdentity); + expect(identityTransitionResult.success, `${caseLabel} canonical identity`).toBe(false); + if (identitySchemaResult.success) { + expect(identityTransitionResult, `${caseLabel} canonical identity`).toMatchObject({ + success: false, + issues: [ + { + path: currentCategory.path, + message: + "A changed branch head requires every head-bound evidence ref to use a fresh canonical identity" + } + ] + }); + } + + const reusedDigest = structuredClone(freshCrossHead); + currentCategory.read(reusedDigest).digest = priorCategory.read(previous).digest; + expect(validateTaskToPrProjectionTransition(previous, reusedDigest).success, `${caseLabel} digest`).toBe(false); + } + }); + + test("keeps owner tombstones monotonic across omission and rejects multi-snapshot ABA reactivation", () => { + const projection = validProjection(); + const present = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "merged", + cleanup: { + eligibility: projection.cleanup.eligibility + } + }); + const absentCursor = ref("replay_cursor", "todos", "3"); + const absent = parseProjection({ + ...present, + id: projectionId("valid-projection"), + cleanup: undefined, + events: { + ...present.events, + replayCursorRef: absentCursor, + sequence: present.events.sequence + 1, + prefixDigest: sha("4") + } + }); + expect(validateTaskToPrProjectionTransition(present, absent)).toEqual({ success: true, issues: [] }); + expect(absent.provenanceLedger.slice(0, present.provenanceLedger.length)).toEqual( + present.provenanceLedger + ); + expect(absent.provenanceLedger.slice(present.provenanceLedger.length).map((entry) => entry.category)).toEqual([ + "replay_cursor", + "replay_prefix" + ]); + + const reactivatedCursor = ref("replay_cursor", "todos", "5"); + const reactivated = parseProjection({ + ...absent, + id: projectionId("valid-projection"), + cleanup: { + eligibility: { + ...present.cleanup!.eligibility, + eventCursorRef: reactivatedCursor + } + }, + events: { + ...absent.events, + replayCursorRef: reactivatedCursor, + sequence: absent.events.sequence + 1, + prefixDigest: sha("7") + } + }); + expect(validateTaskToPrProjectionTransition(absent, reactivated).success).toBe(false); + + const freshCursor = ref("replay_cursor", "todos", "9"); + const freshRotation = TaskToPrProjectionSchema.parse( + withActiveProvenance({ + ...absent, + id: projectionId("valid-projection"), + cleanup: { + eligibility: { + ...present.cleanup!.eligibility, + ref: ref("cleanup_eligibility", "repos", "a"), + status: "preserved", + eventCursorRef: freshCursor + } + }, + events: { + ...absent.events, + replayCursorRef: freshCursor, + sequence: absent.events.sequence + 1, + prefixDigest: sha("d") + } + }) + ); + expect(validateTaskToPrProjectionTransition(absent, freshRotation)).toEqual({ + success: true, + issues: [] + }); + }); + + test("treats ref redaction as exact provenance in ledger prefixes and active bindings", () => { + const previous = parseProjection({ + ...validProjection(), + state: "reviewing", + handoff: undefined, + merge: undefined, + cleanup: undefined + }); + const prefixRedactionMutation = structuredClone(previous); + prefixRedactionMutation.id = previous.id; + const handoffIndex = prefixRedactionMutation.provenanceLedger.findIndex( + (entry) => entry.category === "handoff" + ); + const handoffEntry = prefixRedactionMutation.provenanceLedger[handoffIndex]!; + if (!("ref" in handoffEntry)) { + throw new Error("expected a handoff ref provenance entry"); + } + handoffEntry.ref.redaction = "partial"; + prefixRedactionMutation.events = { + ...prefixRedactionMutation.events, + replayCursorRef: ref("replay_cursor", "todos", "b"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("4") + }; + const prefixCandidate = withActiveProvenance(prefixRedactionMutation); + expect(TaskToPrProjectionSchema.safeParse(prefixCandidate).success).toBe(true); + const prefixCandidateWithStableId = { + ...prefixCandidate, + id: previous.id, + provenanceLedger: prefixCandidate.provenanceLedger.filter( + (entry) => entry.category !== "projection_id" || entry.projectionId === previous.id + ) + }; + expect(validateTaskToPrProjectionTransition(previous, prefixCandidateWithStableId)).toMatchObject({ + success: false, + issues: [ + { + path: "provenanceLedger", + message: + "The provenance ledger is append-only and must retain the previous ledger as an exact immutable prefix" + } + ] + }); + + const activeRedactionMismatch = structuredClone(validProjection()); + activeRedactionMismatch.provenanceLedger = activeRedactionMismatch.provenanceLedger.map((entry) => + entry.category === "handoff" + ? { ...entry, ref: { ...entry.ref, redaction: "partial" as const } } + : entry + ); + expect(TaskToPrProjectionSchema.safeParse(activeRedactionMismatch).success).toBe(false); + }); + + test("retains head-evidence tombstones through five evidence-free lifecycle steps and rejects stale reuse", () => { + const projection = validProjection(); + const reviewed = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "reviewing", + merge: undefined, + cleanup: undefined + }); + const repairing = parseProjection({ + ...reviewed, + id: projectionId("valid-projection"), + state: "repairing", + repair: { + ref: ref("repair_cycle", "todos", "five-step-state"), + cycle: reviewed.repair.cycle + 1, + cap: 2, + exhausted: true, + latestRepairRef: ref("repair_cycle", "todos", "five-step-latest") + }, + events: { + ...reviewed.events, + replayCursorRef: ref("replay_cursor", "todos", "3"), + sequence: reviewed.events.sequence + 1, + prefixDigest: sha("4") + } + }); + expect(validateTaskToPrProjectionTransition(reviewed, repairing)).toEqual({ + success: true, + issues: [] + }); + + const nextHead = head("b"); + const evidenceFree = parseProjection({ + ...repairing, + id: projectionId("valid-projection"), + state: "running", + repository: { + ...repairing.repository, + branchHead: nextHead + }, + exactHead: undefined, + reviews: [], + events: { + ...repairing.events, + replayCursorRef: ref("replay_cursor", "todos", "5"), + sequence: repairing.events.sequence + 1, + prefixDigest: sha("7") + } + }); + expect(validateTaskToPrProjectionTransition(repairing, evidenceFree)).toEqual({ + success: true, + issues: [] + }); + + const stillEvidenceFree = parseProjection({ + ...evidenceFree, + id: projectionId("valid-projection"), + state: "running", + events: { + ...evidenceFree.events, + replayCursorRef: ref("replay_cursor", "todos", "9"), + sequence: evidenceFree.events.sequence + 1, + prefixDigest: sha("a") + } + }); + expect(validateTaskToPrProjectionTransition(evidenceFree, stillEvidenceFree)).toEqual({ + success: true, + issues: [] + }); + + const staleReview = { + ...reviewed.reviews[0]!, + head: nextHead, + reviewedAt: "2026-07-23T15:12:00.000Z" + }; + const staleFifthSnapshot = { + ...stillEvidenceFree, + id: projectionId("valid-projection"), + state: "reviewing" as const, + exactHead: { + ...reviewed.exactHead!, + localHead: nextHead, + remoteHead: nextHead, + providerPullRequestHead: nextHead, + verifiedAt: "2026-07-23T15:12:00.000Z" + }, + reviews: [staleReview], + events: { + ...stillEvidenceFree.events, + replayCursorRef: ref("replay_cursor", "todos", "d"), + sequence: stillEvidenceFree.events.sequence + 1, + prefixDigest: sha("3") + } + }; + expect(TaskToPrProjectionSchema.safeParse(staleFifthSnapshot).success).toBe(false); + expect( + validateTaskToPrProjectionTransition(stillEvidenceFree, staleFifthSnapshot).success + ).toBe(false); + }); + + test("allows same-head review provenance to survive recovery but rejects recovery-time invention", () => { + const projection = validProjection(); + const reviewed = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "reviewing", + merge: undefined, + cleanup: undefined + }); + + function recoverySnapshot( + previous: TaskToPrProjection, + exactHead: TaskToPrProjection["exactHead"], + reviews: TaskToPrProjection["reviews"], + provenanceLedger: TaskToPrProjection["provenanceLedger"], + id: string + ): TaskToPrProjection { + const nextAttempt = { + ...previous.attempt, + ref: ref("attempt", "todos", "a"), + nonce: attemptNonce("review-recovery"), + admissionRef: ref("admission", "codewith", "review-recovery", "partial"), + admissionWriterGenerationRef: ref("writer_generation", "todos", "5"), + workerRef: ref("worker", "codewith", "review-recovery", "partial"), + runtimeRef: ref("runtime", "codewith", "b", "partial"), + writerGenerationRef: ref("writer_generation", "todos", "5"), + writerLeaseRef: ref("writer_lease", "repos", "d", "partial"), + writerFenceRef: ref("writer_fence", "repos", "e", "full"), + providerProfileRef: ref("provider_profile", "codewith", "f", "full"), + providerRouteRef: ref("provider_route", "codewith", "0", "partial") + }; + const recovery = { + ref: ref("recovery", "todos", "7"), + priorAttemptRef: previous.attempt.ref, + priorWriterGenerationRef: previous.attempt.writerGenerationRef, + priorWorkRunRef: previous.workRunRef, + successorAttemptNonce: nextAttempt.nonce, + successorWriterGenerationRef: nextAttempt.writerGenerationRef, + preservedStateRefs: [ + previous.workRunRef, + previous.rootRequestRef, + previous.prGroupRef, + previous.leafTaskRef, + previous.repository.repoRef, + previous.repository.worktreeRef, + previous.repository.branchRef, + previous.events.streamRef, + previous.pullRequestRef! + ], + stopEvidenceRef: evidence("ev_review_recovery_stop", "2"), + leaseRevocationEvidenceRef: evidence("ev_review_recovery_revoke", "3") + }; + return TaskToPrProjectionSchema.parse( + withActiveProvenance({ + ...previous, + id, + state: "recovering", + workRunRef: ref("work_run", "codewith", "1"), + attempt: nextAttempt, + handoff: undefined, + recovery, + exactHead, + reviews, + provenanceLedger, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "4"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("5") + } + }) + ); + } + + const retained = recoverySnapshot( + reviewed, + reviewed.exactHead, + reviewed.reviews, + reviewed.provenanceLedger, + reviewed.id + ); + expect(validateTaskToPrProjectionTransition(reviewed, retained)).toEqual({ + success: true, + issues: [] + }); + + const preReview = parseProjection({ + ...reviewed, + id: projectionId("valid-projection"), + state: "running", + handoff: undefined, + exactHead: undefined, + reviews: [], + provenanceLedger: [] + }); + const invented = recoverySnapshot( + preReview, + reviewed.exactHead, + reviewed.reviews, + preReview.provenanceLedger, + preReview.id + ); + expect(TaskToPrProjectionSchema.safeParse(invented).success).toBe(true); + expect(validateTaskToPrProjectionTransition(preReview, invented).success).toBe(false); + }); + + test("requires a fresh provider guard receipt whenever the guarded expected head changes", () => { + const projection = validProjection(); + const previous = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "merge_ready", + merge: { + guard: projection.merge.guard + }, + cleanup: undefined + }); + const nextHead = head("b"); + const nextReview = { + ...previous.reviews[0]!, + ref: ref("review", "review", "7"), + head: nextHead, + reviewRunRef: ref("review_run", "review", "9"), + proofBundleRef: ref("proof_bundle", "review", "5"), + reviewedAt: "2026-07-23T15:12:00.000Z" + }; + const changedHead = TaskToPrProjectionSchema.parse( + withActiveProvenance({ + ...previous, + id: projectionId("valid-projection"), + createdAt: "2026-07-23T15:12:00.000Z", + repository: { + ...previous.repository, + branchHead: nextHead + }, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "a"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("d") + }, + exactHead: { + ...previous.exactHead!, + localHead: nextHead, + remoteHead: nextHead, + providerPullRequestHead: nextHead, + equalityProofRef: ref("proof_bundle", "review", "3"), + ciProofBundleRefs: [ref("proof_bundle", "review", "4")], + verifiedAt: "2026-07-23T15:12:00.000Z" + }, + reviews: [nextReview], + merge: { + guard: { + ...previous.merge!.guard, + ref: ref("merge_guard", "todos", "a"), + expectedHead: nextHead, + reviewRefs: [nextReview.ref], + proofBundleRefs: [ + nextReview.proofBundleRef, + ref("proof_bundle", "review", "3"), + ref("proof_bundle", "review", "4") + ], + providerGuardReceiptRef: ref( + "merge_guard_receipt", + "merge_provider", + "d", + "full" + ), + evaluatedAt: "2026-07-23T15:12:00.000Z" + } + } + }) + ); + expect(validateTaskToPrProjectionTransition(previous, changedHead)).toEqual({ + success: true, + issues: [] + }); + + for (const staleReceipt of [ + previous.merge!.guard.providerGuardReceiptRef, + { + ...changedHead.merge!.guard.providerGuardReceiptRef, + digest: previous.merge!.guard.providerGuardReceiptRef.digest + } + ]) { + const stale = { + ...changedHead, + id: projectionId("valid-projection"), + merge: { + guard: { + ...changedHead.merge!.guard, + providerGuardReceiptRef: staleReceipt + } + } + }; + expect(validateTaskToPrProjectionTransition(previous, stale).success).toBe(false); + } + }); + + test("binds every merge outcome to its exact immutable merge guard", () => { + const projection = validProjection(); + const guardBoundProjection = projection; + expect(TaskToPrProjectionSchema.safeParse(guardBoundProjection).success).toBe(true); + const { guardRef: _guardRef, ...outcomeWithoutGuardRef } = guardBoundProjection.merge.outcome; + expect( + TaskToPrProjectionSchema.safeParse({ + ...guardBoundProjection, + merge: { + ...guardBoundProjection.merge, + outcome: outcomeWithoutGuardRef + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...guardBoundProjection, + merge: { + ...guardBoundProjection.merge, + outcome: { + ...guardBoundProjection.merge.outcome, + guardRef: ref("merge_guard", "todos", "f") + } + } + }).success + ).toBe(false); + }); + + test("rejects hidden review bindings in denied early-phase merge guards", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "running", + reviews: [], + merge: { + guard: { + ...projection.merge.guard, + decision: "denied" + } + }, + cleanup: undefined + }).success + ).toBe(false); + }); + + test("preserves handoff, recovery, and cancellation identity without embedded history", () => { + const projection = validProjection(); + const recoveryProjection = validRecoveryProjection(); + expect(TaskToPrProjectionSchema.safeParse(recoveryProjection).success).toBe(true); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + recovery: recoveryProjection.recovery + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + handoff: { + ...projection.handoff, + nextWriterGenerationRef: ref("writer_generation", "todos", "f") + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...recoveryProjection, + recovery: { + ...recoveryProjection.recovery, + successorAttemptNonce: attemptNonce("different") + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...recoveryProjection, + recovery: { + ...recoveryProjection.recovery, + preservedStateRefs: recoveryProjection.recovery.preservedStateRefs.filter( + (preservedRef) => preservedRef.id !== recoveryProjection.rootRequestRef.id + ) + } + }).success + ).toBe(false); + + const cancelled = { + ...projection, + state: "cancelled", + recovery: undefined, + exactHead: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + cancellation: { + ref: ref("cancellation", "todos", "a"), + cancelledAttemptRef: projection.attempt.ref, + preservedStateRefs: [ + projection.workRunRef, + projection.rootRequestRef, + projection.prGroupRef, + projection.leafTaskRef, + projection.attempt.ref, + projection.repository.repoRef, + projection.repository.worktreeRef, + projection.repository.branchRef, + projection.events.streamRef, + projection.pullRequestRef + ], + evidenceRefs: [evidence("ev_cancelled_state", "b")] + } + }; + expect(TaskToPrProjectionSchema.safeParse(cancelled).success).toBe(true); + expect( + TaskToPrProjectionSchema.safeParse({ + ...cancelled, + cancellation: { ...cancelled.cancellation, preservedStateRefs: [] } + }).success + ).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + recovery: undefined, + cancellation: { + ref: ref("cancellation", "todos", "d"), + cancelledAttemptRef: projection.attempt.ref, + preservedStateRefs: [ + projection.workRunRef, + projection.rootRequestRef, + projection.prGroupRef, + projection.leafTaskRef, + projection.attempt.ref, + projection.repository.repoRef, + projection.repository.worktreeRef, + projection.repository.branchRef, + projection.events.streamRef, + projection.pullRequestRef + ], + evidenceRefs: [evidence("ev_cancelled_after_merge", "d")] + } + }).success + ).toBe(false); + }); + + test("statically requires fresh canonical ids and digests across handoff and recovery refs", () => { + const projection = validProjection(); + const handoff = projection.handoff; + const recoveryProjection = validRecoveryProjection(); + const recovery = recoveryProjection.recovery; + + expect(TaskToPrHandoffSchema.safeParse(handoff).success).toBe(true); + expect(TaskToPrRecoverySchema.safeParse(recovery).success).toBe(true); + + for (const nextAttemptRef of [ + { ...handoff.nextAttemptRef, id: handoff.previousAttemptRef.id }, + { ...handoff.nextAttemptRef, digest: handoff.previousAttemptRef.digest } + ]) { + expect(TaskToPrHandoffSchema.safeParse({ ...handoff, nextAttemptRef }).success).toBe(false); + } + for (const nextWriterGenerationRef of [ + { ...handoff.nextWriterGenerationRef, id: handoff.previousWriterGenerationRef.id }, + { ...handoff.nextWriterGenerationRef, digest: handoff.previousWriterGenerationRef.digest } + ]) { + expect(TaskToPrHandoffSchema.safeParse({ ...handoff, nextWriterGenerationRef }).success).toBe(false); + } + for (const successorWriterGenerationRef of [ + { ...recovery.successorWriterGenerationRef, id: recovery.priorWriterGenerationRef.id }, + { ...recovery.successorWriterGenerationRef, digest: recovery.priorWriterGenerationRef.digest } + ]) { + expect(TaskToPrRecoverySchema.safeParse({ ...recovery, successorWriterGenerationRef }).success).toBe(false); + } + + const handoffOnly = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "handed_off", + pullRequestRef: undefined, + exactHead: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + evidenceRefs: [] + }); + const recoveryOnly = parseProjection({ + ...recoveryProjection, + id: projectionId("valid-projection"), + state: "recovering", + pullRequestRef: undefined, + exactHead: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + recovery: { + ...recovery, + preservedStateRefs: recovery.preservedStateRefs.filter((preservedRef) => preservedRef.role !== "pull_request") + }, + evidenceRefs: [] + }); + + for (const stoppedWorkRunRef of [ + { ...handoffOnly.handoff!.stoppedWorkRunRef, id: handoffOnly.workRunRef.id }, + { ...handoffOnly.handoff!.stoppedWorkRunRef, digest: handoffOnly.workRunRef.digest } + ]) { + expect( + TaskToPrProjectionSchema.safeParse({ + ...handoffOnly, + handoff: { ...handoffOnly.handoff!, stoppedWorkRunRef } + }).success + ).toBe(false); + } + for (const priorAttemptRef of [ + { ...recoveryOnly.recovery!.priorAttemptRef, id: recoveryOnly.attempt.ref.id }, + { ...recoveryOnly.recovery!.priorAttemptRef, digest: recoveryOnly.attempt.ref.digest } + ]) { + expect( + TaskToPrProjectionSchema.safeParse({ + ...recoveryOnly, + recovery: { ...recoveryOnly.recovery!, priorAttemptRef } + }).success + ).toBe(false); + } + for (const priorWorkRunRef of [ + { ...recoveryOnly.recovery!.priorWorkRunRef, id: recoveryOnly.workRunRef.id }, + { ...recoveryOnly.recovery!.priorWorkRunRef, digest: recoveryOnly.workRunRef.digest } + ]) { + expect( + TaskToPrProjectionSchema.safeParse({ + ...recoveryOnly, + recovery: { + ...recoveryOnly.recovery!, + priorWorkRunRef, + preservedStateRefs: recoveryOnly.recovery!.preservedStateRefs.map((preservedRef) => + preservedRef.role === "work_run" ? priorWorkRunRef : preservedRef + ) + } + }).success + ).toBe(false); + } + }); + + test("freezes complete merge, cleanup, and rollback terminal facts across projections", () => { + const projection = TaskToPrProjectionSchema.parse(validProjection()); + expect( + validateTaskToPrProjectionTransition(projection, { + ...projection, + id: projectionId("valid-projection"), + merge: { + ...projection.merge, + guard: { + ...projection.merge!.guard, + mechanism: "queue_expected_head" + } + } + }).success + ).toBe(false); + expect( + validateTaskToPrProjectionTransition(projection, { + ...projection, + id: projectionId("valid-projection"), + cleanup: { + ...projection.cleanup, + eligibility: { + ...projection.cleanup!.eligibility, + evidenceRefs: [evidence("ev_rewritten_cleanup_gate", "c")] + } + } + }).success + ).toBe(false); + + const rolledBack = TaskToPrProjectionSchema.parse(withActiveProvenance({ + ...projection, + id: projectionId("valid-projection"), + state: "rolled_back", + cleanup: undefined, + rollback: { + plan: { + ref: ref("rollback_plan", "todos", "a"), + targetRef: ref("commit", "repos", "c"), + createdAt: "2026-07-23T15:11:00.000Z" + }, + outcome: { + ref: ref("rollback_outcome", "repos", "d"), + planRef: ref("rollback_plan", "todos", "a"), + targetRef: ref("commit", "repos", "c"), + status: "succeeded", + finishedAt: "2026-07-23T15:12:00.000Z", + evidenceRefs: [evidence("ev_rollback_terminal", "e")] + } + } + })); + expect( + validateTaskToPrProjectionTransition(rolledBack, { + ...rolledBack, + id: projectionId("valid-projection"), + rollback: { + ...rolledBack.rollback, + plan: { + ...rolledBack.rollback!.plan, + createdAt: "2026-07-23T15:11:30.000Z" + } + } + }).success + ).toBe(false); + }); + + test("gates cleanup outcomes on eligibility and preserves non-deletion outcomes", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + cleanup: { + eligibility: { ...projection.cleanup.eligibility, status: "blocked" }, + outcome: projection.cleanup.outcome + } + }).success + ).toBe(false); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "blocked", + merge: undefined, + exactHead: undefined, + reviews: [], + cleanup: { + eligibility: { ...projection.cleanup.eligibility, status: "preserved" }, + outcome: { ...projection.cleanup.outcome, status: "preserved" } + } + }).success + ).toBe(true); + + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + cleanup: { + ...projection.cleanup, + eligibility: { + ...projection.cleanup.eligibility, + targetWorktreeRef: ref("worktree", "repos", "f", "partial") + } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + cleanup: { + ...projection.cleanup, + outcome: { + ...projection.cleanup.outcome, + eligibilityRef: ref("cleanup_eligibility", "repos", "f") + } + } + }).success + ).toBe(false); + }); + + test("binds a single rollback plan and immutable outcome by refs", () => { + const projection = validProjection(); + const rolledBack = withActiveProvenance({ + ...projection, + state: "rolled_back", + cleanup: undefined, + rollback: { + plan: { + ref: ref("rollback_plan", "todos", "3"), + targetRef: ref("commit", "repos", "b"), + createdAt + }, + outcome: { + ref: ref("rollback_outcome", "repos", "c"), + planRef: ref("rollback_plan", "todos", "3"), + targetRef: ref("commit", "repos", "b"), + status: "succeeded", + finishedAt: createdAt, + evidenceRefs: [evidence("ev_rollback", "d")] + } + } + }); + expect(TaskToPrProjectionSchema.safeParse(rolledBack).success).toBe(true); + expect( + TaskToPrProjectionSchema.safeParse({ + ...rolledBack, + rollback: { outcome: rolledBack.rollback.outcome } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...rolledBack, + rollback: { + ...rolledBack.rollback, + plan: { + ...rolledBack.rollback.plan, + targetRef: ref("worktree", "repos", "f", "partial") + } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...rolledBack, + rollback: { + ...rolledBack.rollback, + outcome: { + ...rolledBack.rollback.outcome, + planRef: ref("rollback_plan", "todos", "f") + } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...rolledBack, + rollback: { + ...rolledBack.rollback, + plan: { + ...rolledBack.rollback.plan, + createdAt: "2026-07-23T15:09:59.000Z" + } + } + }).success + ).toBe(false); + }); + + test("accepts local and cloud extensions only as redacted referenced digests", () => { + const projection = validProjection(); + expect(TaskToPrProjectionSchema.parse(projection).adapterExtensions.map((extension) => extension.mode)).toEqual(["local", "cloud"]); + expect(TaskToPrAdapterExtensionSchema.safeParse(projection.adapterExtensions[0]).success).toBe(true); + for (const schema of Object.values(SCHEMA_IDS)) { + expect( + TaskToPrAdapterExtensionSchema.safeParse({ + ...projection.adapterExtensions[0], + schema + }).success + ).toBe(false); + } + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + adapterExtensions: [projection.adapterExtensions[0], projection.adapterExtensions[0]] + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + adapterExtensions: [ + { + ...projection.adapterExtensions[0], + schema: "hasna.task_to_pr_projection.extension.v1" + } + ] + }).success + ).toBe(false); + + const { adapterExtensions: _ignored, ...core } = projection; + const local = parseProjection({ ...core, adapterExtensions: [projection.adapterExtensions[0]] }); + const cloud = parseProjection({ ...core, adapterExtensions: [projection.adapterExtensions[1]] }); + expect(validateTaskToPrAdapterCoreEquivalence(local, cloud)).toEqual({ success: true, issues: [] }); + expect(validateTaskToPrAdapterCoreEquivalence(cloud, local).success).toBe(false); + expect(validateTaskToPrAdapterCoreEquivalence({ ...local, adapterExtensions: [] }, cloud).success).toBe(false); + expect(validateTaskToPrAdapterCoreEquivalence(local, { ...cloud, adapterExtensions: [] }).success).toBe(false); + expect( + validateTaskToPrAdapterCoreEquivalence( + { ...local, adapterExtensions: [projection.adapterExtensions[0], projection.adapterExtensions[1]] }, + cloud + ).success + ).toBe(false); + expect( + validateTaskToPrAdapterCoreEquivalence(local, { + ...cloud, + events: { + ...cloud.events, + sequence: cloud.events.sequence + 1, + replayCursorRef: ref("replay_cursor", "todos", "9"), + prefixDigest: sha("8") + } + }).success + ).toBe(false); + expect( + validateTaskToPrAdapterCoreEquivalence(local, { + ...cloud, + provenanceLedger: [ + ...cloud.provenanceLedger, + { + category: "recovery", + ref: ref("recovery", "todos", "adapter-history") + } + ] + }).success + ).toBe(false); + expect(validateTaskToPrAdapterCoreEquivalence(local, { ...cloud, mutableProviderPayload: true }).success).toBe(false); + }); + + test("binds the expected and provider-observed PR base through exact-head, review, guard, and outcome facts", () => { + const projection = validProjection(); + expect("expectedBase" in projection.exactHead).toBe(true); + expect("providerPullRequestBase" in projection.exactHead).toBe(true); + expect("base" in projection.reviews[0]!).toBe(true); + expect("expectedBase" in projection.merge.guard).toBe(true); + expect("expectedBase" in projection.merge.outcome).toBe(true); + expect("observedBase" in projection.merge.outcome).toBe(true); + const otherBase = head("c"); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + exactHead: { + ...projection.exactHead, + providerPullRequestBase: otherBase + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + reviews: projection.reviews.map((review, index) => + index === 0 ? { ...review, base: otherBase } : review + ) + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + merge: { + ...projection.merge, + guard: { ...projection.merge.guard, expectedBase: otherBase } + } + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + merge: { + ...projection.merge, + outcome: { ...projection.merge.outcome, observedBase: otherBase } + } + }).success + ).toBe(false); + }); + + test("keeps one canonical projection id and rejects identity-only snapshots without owner-event advancement", () => { + const projection = validProjection(); + const previous = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "running", + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + repair: { + ref: ref("repair_cycle", "todos", "identity-stable"), + cycle: 0, + cap: 2, + exhausted: false + }, + provenanceLedger: [], + evidenceRefs: [] + }); + const identityOnly = parseProjection({ + ...previous, + id: projectionId("identity-rotated"), + }); + expect(validateTaskToPrProjectionTransition(previous, identityOnly).success).toBe(false); + + const eventAdvanced = parseProjection({ + ...previous, + id: projectionId("identity-rotated-with-event"), + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "identity-advanced"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("6") + } + }); + expect(validateTaskToPrProjectionTransition(previous, eventAdvanced).success).toBe(false); + }); + + test("enforces the bidirectional state, merge-guard, and merge-outcome matrix", () => { + const projection = validProjection(); + const cancellation = { + ref: ref("cancellation", "todos", "matrix-cancel"), + cancelledAttemptRef: projection.attempt.ref, + preservedStateRefs: [ + projection.workRunRef, + projection.rootRequestRef, + projection.prGroupRef, + projection.leafTaskRef, + projection.attempt.ref, + projection.repository.repoRef, + projection.repository.worktreeRef, + projection.repository.branchRef, + projection.events.streamRef, + projection.pullRequestRef + ], + evidenceRefs: [evidence("matrix_cancel", "7")] + }; + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "cancelled", + merge: { guard: projection.merge.guard }, + cleanup: undefined, + cancellation + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "failed", + cleanup: undefined, + evidenceRefs: [evidence("matrix_failed", "8")] + }).success + ).toBe(false); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "merge_ready", + merge: { + ...projection.merge, + outcome: projection.merge.outcome + }, + cleanup: undefined + }).success + ).toBe(false); + }); + + test("rejects merge readiness and consumed outcomes when admission predates the writer generation", () => { + const projection = validProjection(); + const staleAdmissionAfterRollover = withActiveProvenance({ + ...projection, + state: "merge_ready" as const, + attempt: { + ...projection.attempt, + writerGenerationRef: ref("writer_generation", "todos", "rolled-over") + }, + handoff: undefined, + merge: { + guard: { + ...projection.merge.guard, + decision: "eligible" as const + } + }, + terminalDispositionRef: undefined, + cleanup: undefined, + provenanceLedger: [] + }); + + expect(TaskToPrProjectionSchema.safeParse(staleAdmissionAfterRollover).success).toBe(false); + + const staleAdmissionAtConsumedOutcome = withActiveProvenance({ + ...projection, + attempt: { + ...projection.attempt, + writerGenerationRef: ref("writer_generation", "todos", "consumed-rollover") + }, + provenanceLedger: [] + }); + expect(TaskToPrProjectionSchema.safeParse(staleAdmissionAtConsumedOutcome).success).toBe(false); + }); + + test("preserves eligible merge-guard lineage through revocation or consumption", () => { + const terminalProjection = validProjection(); + const previous = parseProjection({ + ...terminalProjection, + state: "merge_ready", + terminalDispositionRef: undefined, + cleanup: undefined, + merge: { + guard: { + ...terminalProjection.merge.guard, + decision: "eligible" + } + }, + provenanceLedger: [] + }); + const nextEvents = (seed: string) => ({ + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", `guard-lineage-${seed}`), + sequence: previous.events.sequence + 1, + prefixDigest: createHash("sha256").update(`guard-lineage-${seed}`, "utf8").digest("hex") + }); + const terminalDispositionRef = ref("terminal_disposition", "todos", "guard-lineage-terminal"); + const consumedGuard = { + ...previous.merge!.guard, + ref: ref("merge_guard", "todos", "guard-lineage-consumed"), + decision: "consumed" as const + }; + const merged = parseProjection({ + ...previous, + state: "merged", + events: nextEvents("consumed"), + terminalDispositionRef, + merge: { + guard: consumedGuard, + outcome: { + ...terminalProjection.merge.outcome, + guardRef: consumedGuard.ref + } + }, + provenanceLedger: previous.provenanceLedger + }); + expect(validateTaskToPrProjectionTransition(previous, merged)).toEqual({ + success: true, + issues: [] + }); + + const unrelatedGuard = { + ...consumedGuard, + ref: ref("merge_guard", "todos", "guard-lineage-unrelated"), + operatorRef: ref("merge_operator", "merge_provider", "guard-lineage-unrelated", "partial") + }; + const unrelatedMerged = parseProjection({ + ...merged, + events: nextEvents("unrelated"), + merge: { + guard: unrelatedGuard, + outcome: { + ...merged.merge!.outcome!, + guardRef: unrelatedGuard.ref + } + }, + provenanceLedger: previous.provenanceLedger + }); + expect(validateTaskToPrProjectionTransition(previous, unrelatedMerged).success).toBe(false); + + const failedWithoutRevocation = parseProjection({ + ...previous, + state: "failed", + events: nextEvents("dropped"), + terminalDispositionRef, + merge: undefined, + provenanceLedger: previous.provenanceLedger, + evidenceRefs: [evidence("guard_lineage_dropped", "4")] + }); + expect(validateTaskToPrProjectionTransition(previous, failedWithoutRevocation).success).toBe(false); + + const revokedGuard = { + ...previous.merge!.guard, + ref: ref("merge_guard", "todos", "guard-lineage-revoked"), + decision: "revoked" as const + }; + const failedWithRevocation = parseProjection({ + ...previous, + state: "failed", + events: nextEvents("revoked"), + terminalDispositionRef, + merge: { guard: revokedGuard }, + provenanceLedger: previous.provenanceLedger, + evidenceRefs: [evidence("guard_lineage_revoked", "5")] + }); + expect(validateTaskToPrProjectionTransition(previous, failedWithRevocation)).toEqual({ + success: true, + issues: [] + }); + }); + + test("treats failed and blocked dispositions as terminal rather than advertising impossible recovery edges", () => { + const terminalProjection = validProjection(); + const failed = parseProjection({ + ...terminalProjection, + state: "failed", + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + repair: { + ref: ref("repair_cycle", "todos", "terminal-recovery"), + cycle: 0, + cap: 2, + exhausted: false + }, + terminalDispositionRef: ref("terminal_disposition", "todos", "terminal-recovery"), + provenanceLedger: [], + evidenceRefs: [evidence("terminal_recovery_failed", "6")] + }); + const nextAttempt = { + ...failed.attempt, + ref: ref("attempt", "todos", "terminal-recovery-next"), + nonce: attemptNonce("terminal-recovery-next"), + admissionRef: ref("admission", "codewith", "terminal-recovery-next", "partial"), + admissionWriterGenerationRef: ref("writer_generation", "todos", "terminal-recovery-next"), + workerRef: ref("worker", "codewith", "terminal-recovery-next", "partial"), + runtimeRef: ref("runtime", "codewith", "terminal-recovery-next", "partial"), + writerGenerationRef: ref("writer_generation", "todos", "terminal-recovery-next"), + writerLeaseRef: ref("writer_lease", "repos", "terminal-recovery-next", "partial"), + writerFenceRef: ref("writer_fence", "repos", "terminal-recovery-next", "full"), + providerProfileRef: ref("provider_profile", "codewith", "terminal-recovery-next", "full"), + providerRouteRef: ref("provider_route", "codewith", "terminal-recovery-next", "partial") + }; + const recovering = parseProjection({ + ...failed, + state: "recovering", + workRunRef: ref("work_run", "codewith", "terminal-recovery-next"), + attempt: nextAttempt, + events: { + ...failed.events, + replayCursorRef: ref("replay_cursor", "todos", "terminal-recovery-next"), + sequence: failed.events.sequence + 1, + prefixDigest: createHash("sha256").update("terminal-recovery-next", "utf8").digest("hex") + }, + recovery: { + ref: ref("recovery", "todos", "terminal-recovery-next"), + priorAttemptRef: failed.attempt.ref, + priorWriterGenerationRef: failed.attempt.writerGenerationRef, + priorWorkRunRef: failed.workRunRef, + successorAttemptNonce: nextAttempt.nonce, + successorWriterGenerationRef: nextAttempt.writerGenerationRef, + preservedStateRefs: [ + failed.workRunRef, + failed.rootRequestRef, + failed.prGroupRef, + failed.leafTaskRef, + failed.repository.repoRef, + failed.repository.worktreeRef, + failed.repository.branchRef, + failed.events.streamRef + ], + stopEvidenceRef: evidence("terminal_recovery_stop", "7"), + leaseRevocationEvidenceRef: evidence("terminal_recovery_revoke", "8") + }, + terminalDispositionRef: undefined, + provenanceLedger: failed.provenanceLedger, + evidenceRefs: [] + }); + const result = validateTaskToPrProjectionTransition(failed, recovering); + expect(result.success).toBe(false); + expect(result.issues.some((issue) => issue.path === "state")).toBe(true); + }); + + test("requires terminal owner, lease-revocation, and consumed-event facts before deleted cleanup", () => { + const projection = validProjection(); + const previous = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "running", + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + repair: { + ref: ref("repair_cycle", "todos", "cleanup-running"), + cycle: 0, + cap: 2, + exhausted: false + }, + provenanceLedger: [], + evidenceRefs: [] + }); + const deleted = withActiveProvenance({ + ...previous, + id: projectionId("valid-projection"), + state: "cleanup_complete", + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "cleanup-consumed"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("9") + }, + cleanup: { + eligibility: { + ref: ref("cleanup_eligibility", "repos", "cleanup-gate"), + status: "eligible", + targetWorktreeRef: previous.repository.worktreeRef, + eventCursorRef: ref("replay_cursor", "todos", "cleanup-consumed"), + evaluatedAt: createdAt, + evidenceRefs: [evidence("cleanup_gate_without_terminal", "a")] + }, + outcome: { + ref: ref("cleanup_outcome", "repos", "cleanup-deleted"), + eligibilityRef: ref("cleanup_eligibility", "repos", "cleanup-gate"), + targetWorktreeRef: previous.repository.worktreeRef, + status: "deleted", + finishedAt: createdAt, + evidenceRefs: [evidence("cleanup_deleted_without_terminal", "b")] + } + } + }); + expect(TaskToPrProjectionSchema.safeParse(deleted).success).toBe(false); + }); + + test("consumes exactly one repair cycle on entry and freezes repair state after terminal disposition", () => { + const projection = validProjection(); + const exhausted = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "running", + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + repair: { + ref: ref("repair_cycle", "todos", "repair-exhausted-state"), + cycle: 2, + cap: 2, + exhausted: true, + latestRepairRef: ref("repair_cycle", "todos", "repair-exhausted-latest") + }, + provenanceLedger: [], + evidenceRefs: [] + }); + const reentered = parseProjection({ + ...exhausted, + id: projectionId("valid-projection"), + state: "repairing", + events: { + ...exhausted.events, + replayCursorRef: ref("replay_cursor", "todos", "repair-reentry"), + sequence: exhausted.events.sequence + 1, + prefixDigest: sha("a") + } + }); + expect(validateTaskToPrProjectionTransition(exhausted, reentered).success).toBe(false); + + const terminal = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "merged", + cleanup: undefined + }); + const terminalRepairMutation = parseProjection({ + ...terminal, + id: projectionId("valid-projection"), + events: { + ...terminal.events, + replayCursorRef: ref("replay_cursor", "todos", "repair-terminal"), + sequence: terminal.events.sequence + 1, + prefixDigest: sha("b") + }, + repair: { + ref: ref("repair_cycle", "todos", "repair-terminal-state"), + cycle: terminal.repair.cycle + 1, + cap: 2, + exhausted: true, + latestRepairRef: ref("repair_cycle", "todos", "repair-terminal-latest") + } + }); + expect(validateTaskToPrProjectionTransition(terminal, terminalRepairMutation).success).toBe(false); + }); + + test("requires fresh admission and worker-assignment refs on every successor attempt", () => { + const projection = validProjection(); + const previous = parseProjection({ + ...projection, + id: projectionId("valid-projection"), + state: "running", + pullRequestRef: undefined, + exactHead: undefined, + handoff: undefined, + reviews: [], + merge: undefined, + cleanup: undefined, + repair: { + ref: ref("repair_cycle", "todos", "attempt-previous"), + cycle: 0, + cap: 2, + exhausted: false + }, + provenanceLedger: [], + evidenceRefs: [] + }); + const nextAttempt = { + ...previous.attempt, + ref: ref("attempt", "todos", "attempt-next"), + nonce: attemptNonce("attempt-next"), + runtimeRef: ref("runtime", "codewith", "attempt-next", "partial"), + writerGenerationRef: ref("writer_generation", "todos", "attempt-next"), + writerLeaseRef: ref("writer_lease", "repos", "attempt-next", "partial"), + writerFenceRef: ref("writer_fence", "repos", "attempt-next", "full"), + providerProfileRef: ref("provider_profile", "codewith", "attempt-next", "full"), + providerRouteRef: ref("provider_route", "codewith", "attempt-next", "partial") + }; + const successor = parseProjection({ + ...previous, + id: projectionId("valid-projection"), + state: "recovering", + workRunRef: ref("work_run", "codewith", "attempt-next"), + attempt: nextAttempt, + events: { + ...previous.events, + replayCursorRef: ref("replay_cursor", "todos", "attempt-next"), + sequence: previous.events.sequence + 1, + prefixDigest: sha("c") + }, + recovery: { + ref: ref("recovery", "todos", "attempt-next"), + priorAttemptRef: previous.attempt.ref, + priorWriterGenerationRef: previous.attempt.writerGenerationRef, + priorWorkRunRef: previous.workRunRef, + successorAttemptNonce: nextAttempt.nonce, + successorWriterGenerationRef: nextAttempt.writerGenerationRef, + preservedStateRefs: [ + previous.workRunRef, + previous.rootRequestRef, + previous.prGroupRef, + previous.leafTaskRef, + previous.repository.repoRef, + previous.repository.worktreeRef, + previous.repository.branchRef, + previous.events.streamRef + ], + stopEvidenceRef: evidence("attempt_next_stopped", "d"), + leaseRevocationEvidenceRef: evidence("attempt_next_revoked", "e") + } + }); + expect(validateTaskToPrProjectionTransition(previous, successor).success).toBe(false); + }); + + test("versions canonical identity and binds branch plus worktree while retaining the v1 derivation path", () => { + const projection = validProjection(); + const original = deriveTaskToPrIdentityDigest({ + canonicalizationVersion: 2, + rootRequestRef: projection.rootRequestRef, + prGroupRef: projection.prGroupRef, + leafTaskRef: projection.leafTaskRef, + repoRef: projection.repository.repoRef, + worktreeRef: projection.repository.worktreeRef, + branchRef: projection.repository.branchRef, + baseHead: projection.repository.baseHead, + frozenScopeDigest: projection.frozenScopeDigest + }); + const aliased = deriveTaskToPrIdentityDigest({ + canonicalizationVersion: 2, + rootRequestRef: projection.rootRequestRef, + prGroupRef: projection.prGroupRef, + leafTaskRef: projection.leafTaskRef, + repoRef: projection.repository.repoRef, + worktreeRef: ref("worktree", "repos", "identity-alias", "partial"), + branchRef: ref("branch", "repos", "identity-alias"), + baseHead: projection.repository.baseHead, + frozenScopeDigest: projection.frozenScopeDigest + }); + expect(aliased).not.toBe(original); + + const legacyIdentityDigest = deriveTaskToPrIdentityDigest({ + canonicalizationVersion: 1, + rootRequestRef: projection.rootRequestRef, + prGroupRef: projection.prGroupRef, + leafTaskRef: projection.leafTaskRef, + repoRef: projection.repository.repoRef, + baseHead: projection.repository.baseHead, + frozenScopeDigest: projection.frozenScopeDigest + }); + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + canonicalizationVersion: 1, + identityDigest: legacyIdentityDigest + }).success + ).toBe(true); + }); + + test("reserves the v1 adapter-extension namespace independently of unrelated schema registration", () => { + const projection = validProjection(); + expect( + TaskToPrAdapterExtensionSchema.safeParse({ + ...projection.adapterExtensions[0], + schema: "hasna.unrelated_future_contract.v1" + }).success + ).toBe(false); + expect(TaskToPrAdapterExtensionSchema.safeParse(projection.adapterExtensions[0]).success).toBe(true); + }); + + test("rejects duplicate and conflicting same-role recovery or cancellation preservation refs", () => { + const recoveryProjection = validRecoveryProjection(); + expect( + TaskToPrProjectionSchema.safeParse({ + ...recoveryProjection, + recovery: { + ...recoveryProjection.recovery, + preservedStateRefs: [ + ...recoveryProjection.recovery.preservedStateRefs, + ref("root_request", "todos", "conflicting-root") + ] + } + }).success + ).toBe(false); + + const projection = validProjection(); + const cancellation = { + ref: ref("cancellation", "todos", "duplicate-preservation"), + cancelledAttemptRef: projection.attempt.ref, + preservedStateRefs: [ + projection.workRunRef, + projection.rootRequestRef, + projection.rootRequestRef, + projection.prGroupRef, + projection.leafTaskRef, + projection.attempt.ref, + projection.repository.repoRef, + projection.repository.worktreeRef, + projection.repository.branchRef, + projection.events.streamRef, + projection.pullRequestRef + ], + evidenceRefs: [evidence("duplicate_preservation", "f")] + }; + expect( + TaskToPrProjectionSchema.safeParse({ + ...projection, + state: "cancelled", + merge: undefined, + exactHead: undefined, + reviews: [], + cleanup: undefined, + cancellation + }).success + ).toBe(false); + }); + + test("requires distinct stop and lease-revocation evidence identities", () => { + const projection = validProjection(); + expect( + TaskToPrHandoffSchema.safeParse({ + ...projection.handoff, + leaseRevocationEvidenceRef: projection.handoff.stopEvidenceRef + }).success + ).toBe(false); + const recoveryProjection = validRecoveryProjection(); + expect( + TaskToPrRecoverySchema.safeParse({ + ...recoveryProjection.recovery, + leaseRevocationEvidenceRef: recoveryProjection.recovery.stopEvidenceRef + }).success + ).toBe(false); + }); + + test("rejects event cursor sequences beyond Number.MAX_SAFE_INTEGER", () => { + const projection = validProjection(); + expect( + TaskToPrProjectionSchema.safeParse( + withActiveProvenance({ + ...projection, + events: { + ...projection.events, + sequence: Number.MAX_SAFE_INTEGER + 1 + } + }) + ).success + ).toBe(false); + }); + + test("requires structurally nonsemantic owner-resolvable reference ids", () => { + expect( + TaskToPrRefSchema.safeParse({ + ...ref("root_request", "todos", "semantic"), + id: "root_request:opaque-project-alpha-customer-42" + }).success + ).toBe(false); + }); + + test("preserves v1 WorkRun parsing and refuses silent v1 widening", () => { + const legacyWorkRun = { + schema: "hasna.work_run.v1", + id: "run_legacy", + createdAt, + objective: "Legacy compatible run", + status: "running", + actor: { kind: "agent", id: "worker_legacy" } + }; + const parsed = WorkRunSchema.parse(legacyWorkRun); + expect(parsed.schema).toBe("hasna.work_run.v1"); + expect(validateContract(SCHEMA_IDS.workRun, legacyWorkRun).success).toBe(true); + expect( + validateContract(SCHEMA_IDS.workRun, { + ...legacyWorkRun, + taskToPrProjection: validProjection() + }).success + ).toBe(false); + expect(validateContract(SCHEMA_IDS.taskToPrProjection, validProjection()).success).toBe(true); + }); +});