# Workstream 8 Implementation Plan — Remote Publication And Artifact Locks ## Status Completed implementation for Workstream 8, **Remote publication, per-artifact locks, and publish decomposition**, in [`refactoring.md`](refactoring.md). Workstreams 1 through 7 are complete prerequisites. This plan does not enter Workstream 9's cleanup-stage work. ## Authoritative Scope And Fixed Decisions Workstream 8 in [`refactoring.md`](refactoring.md) is authoritative. Its supporting records are H04, H15, H18, and H44 in [`audit.md`](audit.md), C05 in [`contract-audit.md`](contract-audit.md), and their rows in [`audit-decisions.md`](audit-decisions.md). Read those cited sections rather than repeating the audits. The following decisions are final: - Keep `publishStage.Run` as the transaction owner. Split cohesive work into focused files within `internal/stage`; add no framework, distributed transaction, or third-party dependency. - Replace static pipeline locks and the shared remote YAML manifest with one object per normalized stable source at `{session_prefix}/locks/{sha256(source)}.lock`. The digest is lowercase hexadecimal SHA-256 of the exact normalized source bytes. - A lock is strict JSON with `format_version: 1`, `source`, optional `reason`, and an RFC 3339 UTC `created_at`. Define `MaxRemoteLockObjectBytes = 64 << 10` centrally in its package. Reject unknown fields, trailing values, invalid/noncanonical fields, unsupported versions, oversize objects, and payload/key hash mismatches. - `internal/artifacts` owns the payload, identity/key rules, and lock access; `internal/adapters/storage` owns object mechanics; `internal/app` owns commands; and `internal/stage` owns publish use. - Extend `storage.ObjectStore` with idempotent deletion. Listing validates every reserved-prefix object and sorts by source. Add is conditional-create; `--force` makes one generation-matched replacement; remove is idempotent. - Ignore old `locks.yml` objects—do not migrate or delete them. Remove their schema, merge/precedence/retry machinery, runtime injection, and static `pipeline.publish.locks`. Strict config decoding rejects that old field. - The initial lock snapshot controls planning. An unlocked selected output publishes normally. A locked output carries its exact declaration from the prior current commit, if any. A locked output never published remains absent even if configured as required. A lock removed after the initial read may therefore cause one conservative carry. - After immutable objects and the immutable commit are written, reread every initially unlocked relevant source. If any became locked, do not advance the pointer. Attempt objects may remain. A lock created after this final check may race the pointer write; do not add stronger transaction machinery. - A carried `published_output` may reference an earlier run only within the same campaign/session. Preserve source, key, checksum, size, and generation exactly. Derive origin from its validated key; do not add an origin field or copy old bytes. Verify existence, size, and generation without downloading. - Publish metadata and inspection distinguish exactly `published`, `carried_locked`, `locked_unpublished`, `optional_missing`, and `unselected` in one source-sorted `publish_output_outcomes` collection. Preserve upload counts and cleanup-consumed commit/pointer metadata until Workstream 9. - Preserve transaction order: immutable object upload/verification, immutable commit, final lock revalidation, then exactly one current-pointer write. ## Instructions For Every Stage Before each stage: 1. Read `docs/development.md`, all documents under `docs/policy/`, this plan, Workstream 8, its cited audit sections, and the affected sections of [`../internal/stage-publish.md`](../internal/stage-publish.md), [`../internal/storage.md`](../internal/storage.md), [`../config.md`](../config.md), and [`../operations.md`](../operations.md). 2. Inspect affected production code and focused tests. Prefer the codebase knowledge graph for Go discovery/call tracing; use text search for schemas, config, CLI text, docs, fixtures, and removal checks. 3. Confirm worktree state and preserve unrelated changes. Do not implement an adjacent workstream. During each stage, keep focused tests passing, use explicit package-private helpers instead of generic managers/frameworks, keep reads bounded and bodies closed, honor contexts, wrap errors with safe object identity, preserve determinism, and keep tests offline. Run `gofmt` on changed Go files and `git diff --check`. Commit only a completed stage with a short plain-English message. Update documentation alongside observable changes. ## Stage 1 — Add Idempotent Object Deletion **Status:** Completed ### Goal Add the sole storage primitive required to remove one independent lock. ### Required Work 1. Add `Delete(ctx, key) error` to `storage.ObjectStore`. Absence succeeds; preserve existing key validation and contextual errors. 2. Extend the narrow S3 client port and backend using the provider delete-object operation. Propagate cancellation, permission, transport, and other errors. 3. Update every fake/implementation. The standard fake removes object and generation state, is idempotent, honors context, and permits injected-error tests consistently with its other operations. 4. Add no batch, recursive, or lock-specific storage API. ### Verification And Exit Criteria Test success, absence, cancellation, backend failures, and S3 request mapping. Run `go test ./internal/adapters/storage`, `go test ./internal/artifacts ./internal/stage ./internal/app`, and `git diff --check`. All implementations satisfy the narrow idempotent port. ## Stage 2 — Define Independent Lock Objects **Status:** Completed ### Goal Establish the complete lock contract before migrating callers. ### Required Work 1. Add a cohesive `internal/artifacts` owner for payloads, size bound, canonical source validation, key construction, and strict encoding/decoding. 2. Reuse publish's canonical stable-source vocabulary. Reject noncanonical caller input rather than making a second identity. Validate the session prefix before joining the reserved `locks/` path. 3. Implement exact lookup and prefix listing. Exact lookup returns an explicit present/absent result. Listing bounds every read, rejects unexpected names, verifies hashes, rejects duplicate sources, and sorts by source. 4. Implement conditional creation, one generation-matched forced replacement, and idempotent removal. A replacement conflict is reported for operator retry; do not add an internal retry loop. 5. Do not consult static config or `locks.yml`. ### Verification And Exit Criteria Table-test canonical keys, strict JSON, size limits, hash mismatches, absence, invalid prefix entries, ordering, add conflicts, force conflicts/replacement, and removal. Run `go test ./internal/artifacts ./internal/adapters/storage` and `git diff --check`. Each lock is independently manageable and validated. ## Stage 3 — Permit Safe Cross-Run Declarations **Status:** Completed ### Goal Permit exact older published declarations without weakening session isolation. ### Required Work 1. Add a small key parser proving an immutable object belongs to the expected campaign/session and extracting its run. Reject malformed, mutable, traversal, cross-session, and empty-run keys. 2. Relax commit validation only for `published_output`: its key may use the current or an earlier run in the same session. Retain every other placement rule and the existing manifest version unless serialization changes. 3. Update restore, previous-state caching, current-output lookup, and status/artifact inspection to open the exact declared key and match by stable source instead of reconstructing a current-run path. Never scan prefixes. 4. Preserve duplicate-source and destination-collision enforcement when carried and current declarations coexist; derive visible origin from key. ### Verification And Exit Criteria Test same/earlier-run acceptance, malformed/mutable/cross-session rejection, exact-key reads, and source-based inspection. Run `go test ./internal/artifacts ./internal/app ./internal/stage` and `git diff --check`. Only safe same-session published-output carry is enabled. ## Stage 4 — Separate Source And Output Planning **Status:** Completed ### Goal Make source opening and output-rule resolution navigable without changing publish behavior. ### Required Work 1. Move source discovery, metadata construction, safe opening, and validation from the aggregate publish file into one cohesive package-local owner. 2. Move output selection, source matching, required/optional rules, destination resolution, and collisions into another cohesive owner. 3. Keep `publishStage.Run` as caller/transaction owner with explicit inputs; avoid a mutable catch-all context. Do not integrate locks yet. 4. Remove only dead helpers/imports exposed by the mechanical split. ### Verification And Exit Criteria Run `go test ./internal/stage -run Publish`, `go test ./internal/app -run 'Publish|Artifact|Status'`, and `git diff --check`. Existing behavior remains, with clear source/output file-level ownership. ## Stage 5 — Separate Transfer And Commit Mechanics **Status:** Completed ### Goal Finish decomposition while keeping the transaction visible in one place. ### Required Work 1. Move copying/hashing, immutable upload, remote verification, and reusable prior-object verification into a transfer owner. 2. Move immutable commit and current-pointer construction, encoding, and writes into a commit owner. 3. Keep one explicit ordered path in `publishStage.Run`: plan, transfer, immutable commit, pre-pointer checks, pointer write. Document its durability boundary briefly. 4. Delete forwarding wrappers and dead helpers; do not add package-local interfaces without a second implementation. ### Verification And Exit Criteria Preserve checksum, size, generation, conditional-write, cancellation, and partial-failure tests. Run `go test ./internal/stage -run Publish`, `go test ./internal/artifacts ./internal/adapters/storage`, and `git diff --check`. The transaction remains easy to audit from `Run`. ## Stage 6 — Model Locked Output Carry-Forward **Status:** Completed ### Goal Represent every output outcome and build a coherent commit plan without I/O. ### Required Work 1. Add one private output-plan/result type using the five fixed statuses. A participating output is a configured rule selected for this invocation; unselected rules are reported but do not enforce required-output presence. 2. Plan deterministically by source from selected rules, discovered sources, initial locks, and prior current commit. 3. Publish unlocked participants normally. Carry a locked prior declaration byte-for-field. Emit `locked_unpublished` without required-output failure when none exists. Preserve optional-missing and unselected separately. 4. Reject duplicate prior sources, invalid declarations, configured destination conflicts, and effective destination collisions. Never synthesize an old declaration. 5. Project one source-sorted metadata collection with source/status, destination, exact declared key, and derived origin where applicable; omit secrets and local absolute paths. ### Verification And Exit Criteria Pure table tests cover five states, exact carry, required locked absence, mixed snapshots, selection, ordering, and ambiguities. Run `go test ./internal/stage -run 'Publish|Output'` and `git diff --check`. One plan drives both declarations and metadata. ## Stage 7 — Integrate Locks Into Publish **Status:** Completed ### Goal Make independent locks authoritative while preserving the snapshot transaction. ### Required Work 1. Read relevant locks at planning start and load/validate the prior current commit when a locked source may require carry-forward. 2. Use Stage 6's plan. Do not open, hash, or upload a carried or never-published locked source. 3. Before carrying, verify the exact prior object exists and matches declared size and generation without download; retain its declared checksum. 4. Upload/verify new objects, combine exact carried/new declarations, validate and write the immutable commit, then reread initially unlocked sources. If any is now locked, return a clear error without pointer write; otherwise write the pointer exactly once. 5. Remove `Env.RevalidatePublishLocks`, runner injection, and related plumbing once unused. Publish reads authoritative locks through artifact/storage. 6. Replace overlapping category arrays with `publish_output_outcomes`. Preserve upload counts, `remote_commit_key`, and `current_commit_pointer_key` for current cleanup behavior. ### Verification And Exit Criteria Test no locks, exact/mixed carry, required locked absence, invalid prior state, removed-after-initial lock, added-before-final lock, abandoned attempts, avoided locked-source work, and exactly one pointer write. Run `go test ./internal/stage -run Publish`, `go test ./internal/runner ./internal/app`, `go test ./...`, and `git diff --check`. Newly observed final locks prevent pointer advancement. ## Stage 8 — Migrate Commands And Inspection **Status:** Completed ### Goal Preserve the operator workflow over the independent-object model. ### Required Work 1. Rewrite lock list/add/remove services and CLI wiring to use artifact lock operations while preserving command names and session selection. 2. List validated source-sorted locks. Add reports already locked unless `--force`; force performs one conditional replacement. Remove reports idempotent absence without error. Remove help/output references to precedence and shared manifests. 3. Reduce command composition to storage, session prefix, source, reason, clock where needed, and output. Remove config mutation and callbacks. 4. Make status/current-artifact inspection show five outcomes and identify carried sources by exact key and derived run. Fetch only declared objects. 5. Add command tests for exit behavior, conflicts, idempotent remove, deterministic output, malformed locks, and carried inspection. ### Verification And Exit Criteria Run `go test ./internal/app ./internal/artifacts`, `go test ./cmd/...`, and `git diff --check`. Operators retain simple, accurate lock and inspection commands. ## Stage 9 — Remove Retired Lock Authorities **Status:** Completed ### Goal Delete the obsolete control plane after all consumers migrate. ### Required Work 1. Remove `pipeline.publish.locks`, `PublishLockRule`, central store types, `locks.yml` helpers, YAML I/O, merge/precedence logic, document replacement retries, injection fields, compatibility adapters, and dead tests/fixtures. 2. Do not retain migration readers. Remaining historical mentions must be clearly intentional prose. 3. If effective-config digest inputs change, advance its internal discriminator so prior successful state invalidates deliberately. Change no user-facing schema version unless repository policy requires it. 4. Add a representative strict-decode test proving the old config field is unknown, and search code/docs/examples/fixtures for all retired identifiers. ### Verification And Exit Criteria Run: ```sh go test ./internal/config ./internal/artifacts ./internal/app ./internal/stage go test ./... git diff --check ``` Independent objects are the sole lock authority. ## Stage 10 — Align Documentation And Close The Workstream **Status:** Completed ### Goal Make the simplified implementation the fully documented repository state. ### Required Work 1. Update canonical config, storage, stage, CLI, operations, and development docs for the payload/key, bounded validation, commands, carry behavior, race boundary, five outcomes, cross-run rule, and transaction sequence. Remove static-lock and `locks.yml` claims. 2. Document that old `locks.yml` is ignored and may be manually removed after upgrade verification; add no migration tool. 3. Remove stale comments, redundant wrappers/logic, dead fixtures, and obsolete metadata assertions exposed by final review. 4. Run all formatting, unit/integration, static-analysis, documentation, and policy checks required by `docs/development.md`. Record genuinely platform-only checks without weakening or silently skipping them. 5. Only after checks pass, mark this plan's stages and Workstream 8 completed. Leave Workstream 9 as the next pending scope. ### Verification And Exit Criteria At minimum run `gofmt` on changed Go, `go test ./...`, `go vet ./...`, `go test ./internal/doccheck`, `git diff --check`, and all repository scripts mandated by policy. Code, tests, docs, help, and examples agree, and no Workstream 9 implementation is included. ## Open Questions None. The audit decisions and fixed choices above make this plan decision-complete.