diff --git a/docs/releases/v0.8.0.md b/docs/releases/v0.8.0.md new file mode 100644 index 0000000..f3c9d53 --- /dev/null +++ b/docs/releases/v0.8.0.md @@ -0,0 +1,53 @@ +# ReleaseKit v0.8.0 + +## Summary + +ReleaseKit adds read-only recovery for source-publication attempts. It attempts +to retain evidence before a tag push, lets operators discover and explicitly +select a saved attempt, and observes the original destination without repeating +candidate validation or changing tags. Source publication and hosted delivery +continue to have separate outcomes. + +## Publication recovery + +- `Publish` returns reusable `PublicationEvidence` and attempts to save a + private record. The result reports the saved path or a recording error + separately from the publication outcome. Go callers can load or list records + and reconcile retained evidence through the root facade. +- The CLI adds `publication-attempts` for local discovery and `reconcile` for + an explicitly selected record. Both support human-readable and structured + output. `publish --record-dir` selects an alternate record directory, and + failed publication can print a recovery command for the saved attempt. +- Recovery uses the captured endpoint and full tag with current supported Git + transport settings. It reports publication, current absence, conflict, or an + unresolved observation. Current absence does not prove that a version was + never published; published versions and their contents remain immutable. +- Records preserve exact endpoint and path bytes. Ordinary UTF-8 evidence uses + record format 1; format 2 preserves other bytes. Recovery diagnostics redact + credentials, while private records and typed evidence can contain sensitive + endpoints. Operators control record retention and removal. + +## Consumer tools and compatibility + +The module path remains `go.maximumdirect.net/releasekit`, and the Go consumer +minimum remains 1.26.0. Existing publication calls and release profiles remain +supported. Publication now attempts to write a private record by default; a +storage failure is reported separately and does not change the publication +result. Applications handling results should inspect `RecordError` when a saved +recovery handoff matters. + +Consumers using a module-selected CLI can run it with `go tool releasekit`. +The maintained multi-module launcher and Woodpecker examples use the selected +module's tool version. Register the CLI before generating a vendor tree when +using a vendored tool. Older recovery CLIs that only support record format 1 +cannot load format 2 records; use a CLI that supports format 2. + +See the [publication guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/consumers/publish.md), +[CLI reference](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/cli.md), +[publication record formats](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/integrations/publication-records.md), +and [consumer adoption guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/consumers/adoption.md) +for usage and recovery details. + +[COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/COPYRIGHT) | +[LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/LICENSE) | +[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.8.0/docs/oss/attribution.md) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md deleted file mode 100644 index 59c603d..0000000 --- a/docs/roadmap/implementation.md +++ /dev/null @@ -1,594 +0,0 @@ -# Publication reconciliation implementation plan - -Status: complete. - -Implement the target and resolved decisions in the -[feature roadmap](publication-reconciliation.md). The original -[PromptKit request](promptkit-feature-request.md) supplies consumer context. -This document owns implementation sequence and the concrete interface choices -needed for that work. The feature roadmap already separates capability -requirements from staged implementation and remains the owner of intent. - -## Sprint instructions and baseline - -Complete Stage 1 through Stage 8 in order in one end-to-end sprint. Intermediate -stages may leave dependent code incomplete. Continue through the stages without -pilot deployments, intermediate releases, external actions, or human review -gates. Follow the repository's agent and testing policies, including its commit -discipline. Mark completed stages in this document as work lands; retain the -remaining instructions until the sprint finishes. - -Stage verification sections assign coverage to implement; run those checks as -their dependencies become available and continue through temporary compilation -gaps to the assembled validation in Stage 8. - -Start with [development guidance](../development.md) and its applicable policy -reading guide. Relevant implementation locations are: - -| Location | Current responsibility | -| --- | --- | -| `publish.go`, `check.go` | Public publication result, publication facade, and shared release request. | -| `internal/publish/publish.go` | Publication orchestration and immediate `reconcilePush` recovery. | -| `internal/git/remote.go`, `internal/git/tag.go` | Scoped endpoints, transport configuration, URL checks, exact refs, and annotated tags. | -| `internal/git/display.go`, `internal/process` | Credential-safe Git diagnostics and cancellable subprocesses. | -| `cmd/releasekit/main.go` | Command dispatch, repository discovery, publication output, and exit codes. | -| `cmd/releasekit/internal/cli/config.go` | Argument parsing and profile loading. | -| `publish_test.go`, `cmd/releasekit/main_test.go`, `internal/git/remote_test.go` | Existing publication, recovery, CLI, and transport fixtures. | -| `internal/apicheck` | Compiled facade ownership and dependency checks. | - -Keep source publication's successful-push completion point and existing candidate -checks. Later reconciliation and discovery are optional recovery operations. -They observe evidence and configuration, with corrective actions remaining -explicit. Hosted delivery retains its separate workflow. - -Use the existing root facade and internal packages. Put shared recovery and -record mechanics in `internal/publish`, Git mechanics in `internal/git`, and -presentation in the CLI. Public types belong to the root facade; internal -packages depend on internal types. Preserve the small `buildinfo` boundary. - -## Stage 1: Extract the shared observer and classification - -**Completed:** 2026-11-02 - -**Outcome:** Immediate failed-push recovery and a later observation can share -classification without sharing publication prerequisites. - -1. Add `internal/publish/reconcile.go` with internal evidence, observation, and - recovery-error types corresponding to the public shapes specified in Stage 3. - Observation returns its state and available remote object/peeled-commit IDs. - Keep the original evidence separate from the observation. -2. Add an internal Git constructor for observing a captured endpoint. Accept - repository context, the exact endpoint string, and an optional original remote - name. Create a fresh command-scoped remote using the recorded URL for both - read and push URLs. Read the named remote's current supported transport - settings using the existing `transportConfig` path. An empty original name - uses endpoint/global configuration; it does not select `origin` implicitly. -3. Preserve `QueryTag`'s endpoint-resolution verification and exact full/peeled - ref handling. Reading transport settings requires no current fetch/push URL - equality check. Missing settings, including after a remote is renamed or - removed, permit observation; actual configuration-read failures retain their - causes. Reuse the supported transport keys already implemented. Mark - configuration reads and scoped-URL configuration resolution at the Git - boundary so failures map to `transport_configuration` independently of the - optional remote name. Positively detected rewrites use `endpoint_changed`; - other operational failures use `remote_observation`, with cancellation taking - its existing precedence and causes retained throughout. -4. Extract classification so the immediate path can supply its already captured - endpoint and transport settings. Keep `reconcilePush` as the publication - adapter: retain its existing detached, bounded recovery context, phase - transitions, and original push-error chain. A successful exact match still - resolves that push error. Successful pushes continue to return directly. -5. The later path uses the caller's context for configuration reads and the - observation. Use the evidence's `RepositoryDir` as Git working-directory - context; an empty value uses the caller's current directory. This preserves - the meaning of relative local endpoints in automatically captured records. - An unavailable recorded directory produces an unresolved observation with - actionable context. Recovery does not discover identity from current HEAD, - local tags, module files, profiles, or current remote URLs. - -Use this classification order for later recovery: - -| Evidence or observation | State and error behavior | -| --- | --- | -| Missing endpoint or full tag | `unknown`, with `missing_evidence`; list the missing fields and perform no remote query. | -| Invalid supplied full object ID or invalid Git tag ref | `unknown`, with `invalid_evidence`; perform no remote query. | -| Cancellation or deadline prevents completion | `unknown`, with `canceled` and the context cause. | -| Endpoint rewriting changes the captured URL | `unknown`, with `endpoint_changed`; do not query the substituted destination. | -| Transport-configuration read fails | `unknown`, with `transport_configuration`. | -| Query fails or returns unreadable evidence | `unknown`, with `remote_observation`. | -| Exact-ref records are inconsistent | `unknown`, with `inconsistent_observation`. | -| Full tag is authoritatively absent | `not_published`; the observation completed successfully, even if tag-object or candidate-commit evidence is missing. | -| Full tag is present and intended tag-object ID is missing | `unknown`, with `missing_evidence`, including when a commit matches. | -| Observed tag-object ID differs from the intended ID | `conflict`; include both IDs. | -| Tag-object IDs match and no peeled ref is available | `published`, preserving existing behavior. | -| Tag-object IDs match, a peeled ref is available, and candidate commit is missing | `unknown`, with `missing_evidence`. | -| Tag-object IDs match and the peeled commit matches the candidate | `published`. | -| Tag-object IDs match but the peeled commit differs | `conflict`; include expected and observed commit IDs as well as tag-object IDs. | - -Reuse existing full SHA-1/SHA-256 ID and Git-ref validation through internal -helpers. Compare hexadecimal IDs by value, accepting their existing supported -letter cases; retain supplied evidence for reporting. Module/version labels and -recording time are context, rather than additional observation prerequisites. -Distinguish a positively invalid ref from tooling, directory, configuration, -or cancellation failures while checking it; operational failures retain their -appropriate unresolved cause. Expose the existing diagnostic formatter through -an internal Git helper for shared error/presentation use, preserving wrapped -causes and the current credential-redaction behavior. - -For the later API, a completed observation of `published`, `not_published`, or -`conflict` returns a nil error. An unresolved observation returns an error and -its `unknown` result. The immediate publication adapter continues to preserve -the original push error for known absence/conflict and unresolved recovery. - -**Verification:** Extend existing classification tests for the partial-evidence -matrix and expected/observed IDs. Reuse existing Git parser tests for unchanged -duplicate-ref and peeled-only behavior. Add focused Git-boundary coverage for -current transport settings, repaired settings, removed/renamed remotes, fixed -URLs, and configuration-read failure. Preserve the existing lost-acknowledgment, -rejected-push, endpoint-change, and cancellation behavior. - -**R16 follow-up completed:** Configuration, rewrite, and operational failures -map by their known cause, with facade/CLI coverage for named and unnamed -configuration, positive rewrites, and tooling failures. Setup failure and rewrite -mismatch tests confirm that no remote tag query occurs. - -## Stage 2: Implement private attempt records - -**Completed:** 2026-11-02 - -**Outcome:** A supported internal codec can save and load a single attempt -without relying on diagnostic output. - -1. Add record encoding and storage helpers under `internal/publish`. Use a JSON - envelope with integer `format_version` and an `evidence` object. Ordinary - UTF-8 evidence uses version 1; non-UTF-8 fields use version 2 and a separate - `evidence_bytes` object of base64 strings, preserving exact supported bytes. - See the [canonical format](../integrations/publication-records.md#private-attempt-record). - The evidence fields - are `repository_dir`, `module_dir`, `module_path`, `version`, `tag`, - `candidate_commit`, `tag_object`, `endpoint`, `remote`, and `recorded_at`. - Store the exact endpoint. Fields other than the envelope version/object can - be missing in a partial record. Retain the evidence available at capture; - loading does not fill omissions from current repository state. -2. Decode one complete JSON document. Missing/unsupported envelope versions, - malformed JSON, and structural type errors are `record_format` failures. - Accept additional fields within a supported version. Semantic evidence - completeness is evaluated by reconciliation, so a readable partial record - remains usable for discovery and any classification it can support. Decode - known version-2 byte fields as strings, then decode standard base64. Invalid - structural types and malformed base64 return `record_format` with their - available causes. Additional unknown fields retain their existing handling. -3. Resolve the default directory with - `git rev-parse --path-format=absolute --git-common-dir`, then append - `releasekit/publication-attempts`. Use Git metadata discovery rather than - assuming `.git` is a directory. This shares saved attempts across linked - worktrees of the repository. Repository discovery for storage needs no - candidate branch, cleanliness, or HEAD check. -4. Treat the override as a directory. An absolute override is used directly; - a relative override is resolved against the source repository root. Record - files have distinct `attempt-.json` names. Use standard-library random - naming and exclusive file creation to keep concurrent calls and retries from - overwriting each other. The filename supplies uniqueness; time is discovery - metadata, recorded once per attempt as UTC RFC3339Nano text. -5. Create ReleaseKit-owned directories privately and files with owner-only - permissions, using platform-supported permissions. Apply this file handling - equally to default and overridden storage. Keep unrelated existing directory - permissions intact. Use ordinary encoding, writes, closes, and failed-write - cleanup; this is a small evidence file with the durability limits in the - feature roadmap. Report write/close failures rather than presenting a failed - write as a saved record. Report an absolute record path on successful save. -6. Preserve publication's existing clean-worktree requirement when resolving an - override. Git metadata, external locations, and Git-ignored locations can - retain records. If writing the proposed record would introduce unignored - worktree contents, report `record_write` and leave that record unwritten. - Use the actual proposed file path and Git's ignore rules before creating - record contents; account for ordinary filesystem path resolution. This is a - recording failure, not a new publication prerequisite or a reason to change - ignore configuration automatically. Do not modify tracked files or exclusion - rules to accommodate storage. -7. File-open/read failures are `record_read`; storage-resolution and save - failures are `record_write`. Preserve underlying causes through wrapping. - Error displays identify paths without echoing record contents. Retention and - manual removal remain operator-owned. - -**Verification:** Use real temporary storage for complete/partial round trips, -unsupported versions, malformed data, read/write failures, unique attempts, -default and override paths, and permissions where supported. Verify linked -worktree metadata resolution and clean-tree preservation for an external, -ignored, and unignored override. Use injected failures at the small file/Git -boundary when platform permissions cannot reliably induce a failure. Verify -that private records retain synthetic credentials while diagnostics redact them. - -**R17 follow-up completed:** Known byte fields validate their string representation -before base64 decoding. Tests cover valid/empty strings, numeric arrays, other -structural errors, and retained JSON/base64 causes. Existing all-field byte -round trips and original-versus-decoy Git endpoint observations remain covered. - -## Stage 3: Add the facade-owned recovery interfaces - -**Completed:** 2026-11-02 - -**Outcome:** Go callers can pass publication evidence directly to recovery or -load it through ReleaseKit. - -Define these root-facade interfaces, with ordinary conversions to internal types: - -```go -type PublicationEvidence struct { - RepositoryDir string - ModuleDir string - ModulePath string - Version string - Tag string - CandidateCommit string - TagObject string - Endpoint string - Remote string - RecordedAt string -} - -type PublicationReconciliationResult struct { - Evidence PublicationEvidence - State PublicationState - ObservedTagObject string - ObservedCommit string -} - -type PublicationRecoveryError struct { - Kind string - RecordPath string - MissingFields []string - // Private cause, with Error and Unwrap methods. -} - -func ReconcilePublication(context.Context, PublicationEvidence) (PublicationReconciliationResult, error) -func LoadPublicationEvidence(string) (PublicationEvidence, error) -``` - -1. Implement `ReconcilePublication` using Stage 1 and the existing public - publication states. It performs observation only; Stage 1 defines its result - and error semantics. `LoadPublicationEvidence` uses Stage 2 and reads the - explicitly supplied path, relative to the caller's working directory when - relative. It performs no Git discovery or remote access. -2. Implement the facade-owned error with pointer receivers for `Error` and - `Unwrap`. Convert internal recovery errors while retaining original causes - for `errors.Is`/`errors.As`. Document `Kind` values from Stages 1 and 2 plus - `storage_enumeration` from Stage 5. `MissingFields` uses the record field - names. Provide credential-safe error text through the existing Git diagnostic - redaction machinery; raw evidence and unwrapped causes remain sensitive. -3. Add `PublicationRecordDir string` to `ReleaseRequest`, documenting it as a - publication-only directory override that `Check` ignores. Add - `Evidence PublicationEvidence`, `RecordPath string`, and `RecordError error` - to `PublicationResult`. Preserve existing flat fields and their behavior; - populate them and the new evidence from the same captured values. Stage 4 - wires recording into publication. `Publish` is the supported record producer. -4. Keep serialization in the internal codec. The facade's evidence is the - reusable typed input, not the CLI's diagnostic JSON representation. Document - its raw endpoint and the sensitive nature of retained records. - -**Verification:** Add external-package API tests for direct evidence recovery, -record loading, partial evidence, preserved causes, and state/error semantics. -Include a caller using `result.Evidence` and a caller using the loader's result -without copying fields. Extend the existing facade checks only as needed to -protect ownership of these types and existing dependency directions; keep the -current external-type allowances. - -## Stage 4: Record attempts inside shared publication - -**Completed:** 2026-11-02 - -**Outcome:** API and CLI publication retain the same evidence before pushing, -with recording failures separate from the publication outcome. - -1. Wire the new request/result fields through the facade and - `internal/publish.Request`/`Result`. Keep recording in `internal/publish`. - Use named result returns in the internal publication operation so finalization - can add record information to every returned result without replacing the - publication error. -2. Initialize attempt metadata when candidate capture has established the source - repository, including when capture returns partial evidence with an error. - Capture the actual effective remote name, using publication's existing - `origin` default. Discover storage locally and retain any storage failure - separately. An invocation that fails before its repository is known returns - its available typed evidence with no initialized record or recording error. -3. Populate evidence from the captured candidate, resolved endpoint, and created - tag-object ID as those become available. Preserve tag-object evidence returned - alongside a tag-inspection failure. Use the stable attempt timestamp from - initialization and the same evidence value in the result and saved record. -4. Save one evidence snapshot after the final existing pre-push guards and - before calling `PushTag`. On an earlier ordinary return, save the available - partial snapshot during finalization instead. Finalization runs once for - recording purposes; it does not retry or create another attempt after a - failed save. An interrupted push can therefore leave a complete saved identity - even when the process never returns a publication result. -5. Preserve the original publication return value and error when storage fails. - Supply the wrapped recording failure in `RecordError`, with `RecordPath` - populated only for a saved record. A successful push with a failed save still - returns publication success. A failed/unknown push with a saved record keeps - its existing state and original error chain. -6. Records contain original identity and discovery metadata. Later observations - and publication's final outcome remain separate results. Reconciliation and - listing read saved records without updating them. Each later publication - invocation gets its own attempt; existing records are retained. - -**Verification:** Extend public publication fixtures to assert that the record -exists and contains the intended endpoint, full tag, commit, and tag object -before the push subprocess proceeds. Exercise success, rejected push, unresolved -push, pre-push failure, tag-inspection failure, and storage failure. Verify -unchanged publication outcomes/cause chains, one useful partial record on early -failure, distinct records for retries/modules, and no standard-suite duplication. -Verify that default storage and a failed unignored override keep the candidate -worktree clean. Cover a non-nil `RecordError` on successful publication through -both API results and Stage 7's CLI integration, preserving publication success. - -## Stage 5: Provide local attempt discovery - -**Completed:** 2026-11-02 - -**Outcome:** Callers can find saved attempts even after losing publication -output, with useful partial discovery when a record is damaged. - -Add these facade-owned types and helper, backed by `internal/publish`: - -```go -type PublicationAttemptsRequest struct { - RepositoryDir string - RecordDir string -} - -type PublicationAttempt struct { - RecordPath string - Evidence PublicationEvidence -} - -type PublicationAttemptsResult struct { - RecordDir string - Attempts []PublicationAttempt - Diagnostics []*PublicationRecoveryError -} - -func ListPublicationAttempts(context.Context, PublicationAttemptsRequest) (PublicationAttemptsResult, error) -``` - -1. An empty repository selection means the current directory. Resolve the - default or relative override using Stage 2's repository/storage rules. An - absolute `RecordDir` can be enumerated directly, including outside a checkout. - Discovery neither creates the directory nor requires a release profile. -2. Enumerate the selected directory's JSON record files at that level, using - the shared loader. Return readable attempts with absolute paths, including - partial records. Return a per-record recovery diagnostic for each unreadable, - malformed, or unsupported record, and continue to other entries. Use a - deterministic path order for presentation without introducing automatic - chronological selection. -3. Return a top-level `storage_enumeration` error for directory-resolution or - enumeration failure, including a missing selected storage directory. Preserve - already obtained entries/diagnostics when available. An existing readable - empty directory returns an empty successful result. Cancellation uses the - `canceled` kind and preserves partial results and its cause. Per-record failures - alone populate `Diagnostics` with a nil top-level error. -4. Listing reads saved identity and recording time. It performs no remote - query, current-candidate capture, local-tag inspection, recovery selection, - or cleanup. Error text follows Stage 3's credential-safe behavior. - -**Verification:** Cover multiple modules/retries, empty storage, mixed readable -and damaged records, unsupported versions, inaccessible/missing directories, -explicit external storage, and cancellation. Verify no remote access and no -record mutation. Test public result/error distinctions and preserved causes; -reuse codec cases rather than repeating all parser permutations here. - -## Stage 6: Add CLI recovery, discovery, and structured output - -**Completed:** 2026-11-02 - -**Outcome:** Operators and automation can discover and reconcile explicitly -selected attempts without profiles or diagnostic parsing. - -Implement these command forms in the existing parser and CLI dispatch: - -```text -releasekit reconcile --record FILE [--json] -releasekit publication-attempts [--repo DIR] [--record-dir DIR] [--json] -``` - -1. Add command/argument fields to `cli.Invocation` and extend usage/parser tests. - Neither command takes a version positional argument or loads a profile. - Reconciliation requires its explicit record path. The loader resolves a - relative record path from the invoking directory; the evidence supplies the - original repository context for observation. Listing uses Stage 5's directory - rules. Route these commands before `main.run`'s current unconditional - repository-root lookup for other non-version commands. -2. Place recovery presentation in a focused CLI source file rather than growing - candidate/build/delivery handlers. Invoke the root facade for load, observe, - and list mechanics. A record-read/format error preserves the requested path - and produces an invocation error. A readable partial record reaches the - observer and can produce `unknown` or a supported known observation. -3. Human output shows the selected path, safe identity, state, available observed - IDs, and actionable cause context. Unknown diagnostics identify missing - fields, invalid evidence, changed endpoint rewriting, transport-configuration - failure, cancellation, or failed/inconsistent observation using the typed - recovery error. Show expected and observed IDs for conflicts, including - commit evidence when that is what conflicts. Explain possible next actions - from known causes, without guessing that a generic Git failure is an - authentication or connectivity failure. - Configuration failures include endpoint/global settings when the remote name - is absent. Use the same cause-based recovery kinds from Stage 1 for JSON - diagnostics and human guidance. -4. Human listing shows path, module identity, full tag, candidate commit, and - recording time when available. Show per-record diagnostics alongside usable - entries and make incomplete discovery clear. Distinguish an enumeration - failure from a successfully enumerated empty directory. -5. Implement explicit CLI JSON DTOs. After successful argument parsing, - `--json` writes one JSON document to stdout for success and failure paths; - normal usage/argument-parse errors retain the existing stderr behavior. - Use these fields, with `schema_version: 1` and the command name: - - | Command | JSON fields beyond `schema_version` and `command` | - | --- | --- | - | `reconcile` | `record_path`, safe `evidence`, `state`, `observed_tag_object`, `observed_commit`, and `diagnostic` when unresolved or loading fails. Omit observation fields when loading failed. | - | `publication-attempts` | `record_dir`, `attempts` (each with `record_path` and safe `evidence`), `diagnostics`, and a top-level `diagnostic` for enumeration/cancellation failure. Arrays are present, including when empty. | - - Safe evidence uses the Stage 2 snake-case field names with credential-safe - string displays. A diagnostic contains `kind`, `message`, `record_path` when - applicable, and `missing_fields` when applicable. Encode error text explicitly; - keep raw Go errors and private record JSON out of these DTOs. Apply existing - URL/diagnostic redaction before JSON encoding, including nested messages. - Raw endpoint preservation belongs only to private records and typed evidence. - Human and JSON output use the same state/diagnostic interpretation. -6. Use these process statuses and document their distinct meanings: - - | Command/result | Status | - | --- | --- | - | Reconciliation confirms publication | 0 | - | Reconciliation observes absence or conflict | 1 | - | Reconciliation invocation, selected-record read, or format failure | 2 | - | Reconciliation remains unknown, including missing evidence or canceled observation | 3 | - | Discovery completes with readable entries and no diagnostics, including an empty existing store | 0 | - | Discovery is incomplete or fails, including cancellation | 1 | - | Discovery argument/usage error | 2 | - - A nil Go observation error for absence/conflict still maps to CLI status 1. - Preserve the original context or filesystem cause in the API even when its - CLI status is an invocation classification. - -**Verification:** Add parser/dispatch tests and command tests for human/JSON -state parity, exit classifications, mandatory selection, unusable records, -partial discovery, and credential redaction. Assert semantic fields rather than -complete diagnostic wording. Test paths with spaces/apostrophes and running -record-based recovery from another working directory. Verify these commands -work with a dirty worktree, changed branch/HEAD, removed or moved local tag, -and unavailable profile, with no candidate commands or hosted requests. - -## Stage 7: Integrate publication's record override and recovery handoff - -**Completed:** 2026-11-02 - -**Outcome:** Existing publication commands expose saved evidence and recording -failures without additional consumer release infrastructure. - -1. Add `--record-dir DIR` to `publish`, mapping it to - `ReleaseRequest.PublicationRecordDir`. Keep the automatic default sufficient - for ordinary callers. The override is a CLI/API option; profile JSON remains - unchanged for this feature. The flags for other existing commands retain - their meanings. -2. After publication, report a saved absolute record path or a credential-safe - recording warning on stderr, separately from the existing publication - summary/error. These messages leave publication's exit classification and - original failure diagnostics intact, including success with recording failure. -3. When publication returns an error at tag creation, push, or immediate - reconciliation and a record was saved, print a copyable `reconcile --record` - command selecting that exact path. Partial records can explain missing - evidence. Earlier failures still report their saved path when available. - Successful publication reports the path without prescribing a recovery step. -4. Prefer the consumer module's registered Go tool for the printed command. - API-only consumers use durable private module/checksum files under Git - metadata, with a separate bootstrap module root so consumer vendoring does - not govern tool resolution. Retain the selected requirements, checksums, - versioned replacements, and original locations of local replacements. - Consumers without a selected ReleaseKit dependency use the native executable - path. Select the absolute record path explicitly and quote every argument - for the POSIX-shell runbooks, including apostrophes. The command survives a - cold Go-tool executable's removal and launcher-owned metadata cleanup. - Explain how to use another selected supported CLI if an entry point becomes - unavailable. -5. Prepare the command after publication using a separate bounded context so - publication cancellation leaves handoff construction available. Report - module-preparation failures separately and retain a persistent native - executable fallback. Recognized temporary Go-build executables provide no - durable native fallback. Keep publication's original status/error and - immediate reconciliation context unchanged. - -**R14/R15 follow-ups completed:** API-only recovery uses an independent private -module root; post-failure preparation handles cancellation and native fallback. - -**Verification:** Extend existing publication CLI tests for the default, override, -recording warning with successful status, and original failed/unknown status. -Use a built CLI in an assembled test to execute the printed recovery command -and verify it selects the original record among retries/modules. Test shell -argument quoting with synthetic paths. Keep the existing module-selected Go-tool -launchers and source bootstrap working without adding consumer-owned recording. -Include an API-only consumer that vendors the library without declaring the CLI -tool; execute its handoff and verify source/vendor contents remain unchanged. -Exercise relative local replacements, versioned replacements, and ReleaseKit's -own source-module recovery entry point. -Coordinate cancellation at the recorded push boundary for native, cold -registered-tool, and temporary-launcher invocations. Execute the resulting -handoff after driver/launcher cleanup and verify retained publication diagnostics -and recovery statuses. Exercise native fallback after module-preparation failure. - -## Stage 8: Finish assembled coverage and canonical documentation - -**Completed:** 2026-11-02 - -**Outcome:** The full feature works across process boundaries, with supported -contracts and a short consumer recovery workflow. - -1. Add focused assembled tests for publication with a failed/interrupted push, - lost output followed by discovery, and a separate later recovery invocation. - Use a controlled local Git boundary to know when pre-push recording finished; - coordinate cancellation/interruption without arbitrary sleeps. Remove or move - the local tag and change branch, HEAD, remote URLs, or transport settings - before later recovery. Cover root/nested tags and original-record selection. - Retain the assembled vendored API-only handoff coverage added for R14, - alongside cold registered-tool and temporary-launcher recovery. - R15 adds canceled publication and preparation-failure handoff cases using - explicit push readiness and fresh later invocations. - R16 covers setup failure classifications and human/JSON guidance without - querying an invalid or substituted endpoint. - R17 adds byte-field structural and encoding failures at the codec boundary, - reusing the existing partial-record and lossless-observation fixtures. -2. Establish read-only behavior through resulting refs, worktree/branch state, - saved records, and controlled observations of candidate/hosted boundaries. - Reuse lower-level classification and codec tests. Tests use local remotes, - temporary storage, synthetic credentials, and isolated environments; live - services and real credentials are unnecessary. -3. Update GoDoc for evidence, results, errors, request fields, and all new helpers. - Clarify existing publication-state comments where they currently describe - only successful pushes or a single invocation, while preserving those - states' established immediate-publication behavior. -4. Update documentation at its canonical owners: - - | Owner | Change | - | --- | --- | - | `docs/cli.md` | New commands, flags, explicit selection, relative-path semantics, human/structured modes, and statuses; update the command inventory. | - | `docs/consumers/publish.md` | Automatic recording, separate recording failures, reusable typed evidence, loader/discovery use, optional later observation, storage placement/lifecycle, overrides, permissions, retention, and sensitive-data handling. | - | New `docs/integrations/publication-records.md` | Versioned private-record and CLI JSON formats and unsupported-format handling. Link to CLI/GoDoc for operation contracts and the publication guide for physical placement/lifecycle. | - | `docs/release.md` | Replace manual endpoint-copy/comparison instructions with explicit-record recovery and local discovery; describe lost/older evidence and unavailable original executable. | - | `docs/internal/overview.md` | Implemented observer, recording, discovery, facade, and CLI responsibilities; link to canonical formats and contracts. | - - The runbook must explain that current absence does not prove a version was - never published or permit reuse of a known published version. Keep cleanup - and retry choices explicit and preserve immutable published tags/contents. - Storage failure loses the saved handoff, rather than changing the actual - publication outcome. Explain that direct typed evidence can still support - recovery when recording fails. Document the limits of abrupt process - termination before recording, removed storage, and older attempts lacking - sufficient retained identity. Later recovery adds no successful-publication - read-back, polling, CI, installation, or hosted-delivery gate. -5. Review examples, local links, fragment destinations, privacy language, and - consistency with the feature roadmap. Keep release-version selection and - tagging in later release preparation. Update any existing draft release note - only when its version/scope has been deliberately selected for this feature. -6. Once the assembled implementation is ready, run `go test ./...` and - `go run ./internal/apicheck` with the prepared compatible local Go toolchain - and offline dependencies. Run focused tests while implementing their owners; - reuse successful checks for unchanged content. Add targeted race detection - if new shared mutable state or concurrent record tests expose a distinct risk. - Finish with `git diff --check`, review all implementation/test/doc changes, - and complete the repository's normal commit discipline. - -The sprint is complete when automatic API/CLI recording, direct and loaded -evidence reconciliation, partial local discovery, current transport settings -with fixed original endpoints, safe human/structured output, and the assembled -recovery workflow satisfy this plan and the feature roadmap. The final report -should identify implemented interfaces, validation results, and any remaining -limitations. Subsequent architecture review and release/testing decisions take -place after this complete initial implementation. - -**Follow-up verification completed:** R14–R17 corrections and their assigned -regression coverage are complete. The vendored API-only workflow runs through -a private Go-tool launcher, removes the original launcher metadata, and then -executes its printed recovery command. Native and cold Go-tool cancellation, -fallback, setup-error classifications, and byte-field format validation are -covered. The full offline suite and compiled facade/dependency checks passed -with Go 1.26.8; prose whitespace, local links, and fragment destinations were -checked. See [remediation completion records](remediation.md). diff --git a/docs/roadmap/promptkit-feature-request.md b/docs/roadmap/promptkit-feature-request.md deleted file mode 100644 index 30d1732..0000000 --- a/docs/roadmap/promptkit-feature-request.md +++ /dev/null @@ -1,80 +0,0 @@ -# Promptkit Feature Request: Read-Only Publication Reconciliation - -## Consumer Context - -Promptkit uses ReleaseKit v0.7.0 as a module-selected Go tool for candidate -validation, source-tag publication, and page-only Gitea delivery. Source -publication and hosted delivery have separate outcomes. A successful intended -tag push completes the source release without waiting for CI or module -availability. - -ReleaseKit already reconciles an unsuccessful push with a bounded authoritative -query. This request concerns later recovery when that observation remains -unknown and the maintainer can investigate again after connectivity or -authentication is restored. - -## Current Pain Point - -Promptkit's runbook reproduces a manual recovery procedure: retain the captured -publication endpoint, version, candidate commit, and tag object; check that Git -URL rewriting still resolves the endpoint to itself; query the exact tag; then -interpret whether the original tag was published, absent, conflicting, or still -unobservable. - -That procedure is generic release logic, rather than Promptkit-specific policy. -Keeping it in each consumer's instructions adds maintenance and opportunities -for mistakes. Querying the current remote instead of the original captured -endpoint can establish the wrong outcome after configuration changes. - -Credential-safe diagnostics introduce an additional handoff issue: a displayed -HTTP(S) endpoint can have its userinfo redacted. That display is not necessarily -a usable transport URL, and asking an operator to reconstruct the original -endpoint from it is inconvenient. Consumers should not need to parse diagnostic -text or independently implement ReleaseKit's Git observation rules. - -## Requested Capability - -Please consider a first-class, read-only CLI operation for reconciling a previous -source-publication attempt. A name such as `reconcile` or `publication-status` -would be suitable; the exact command and interface are for ReleaseKit to choose. -A corresponding public Go API could serve library consumers with the same need. - -The capability would use the original attempt's evidence, including the exact -captured endpoint, complete Git tag, intended tag-object identity, and candidate -commit as applicable. It should: - -- Classify confirmed publication, authoritative absence, a conflicting tag, and - an unresolved observation using ReleaseKit's existing publication semantics. -- Preserve the distinction between the annotated tag object and its peeled - source commit; a commit match alone does not establish the intended tag object. -- Account for endpoint rewriting and configuration changes rather than silently - substituting the current remote destination. An observation failure or - insufficient original evidence should remain unresolved. -- Offer a practical handoff of attempt evidence without requiring consumers to - parse human-readable output or retype a credential-bearing URL from a - redacted diagnostic. Authentication can remain locally supplied, and displayed - endpoints should retain the existing credential-safe treatment. -- Return documented, actionable outcomes for operators and automation. - -Promptkit has no preference for a particular evidence-storage format or internal -implementation. Explicit inputs or a reusable attempt record could meet the -need; the important property is that recovery refers to the original attempt. - -## Scope And Workflow - -This is a diagnostic operation after a failed or uncertain publication. It -would observe remote state without creating, pushing, moving, or deleting tags; -changing branches; publishing hosted pages; or rerunning candidate validation. -Any subsequent corrective action would remain explicit. - -It would not become a read-back gate after a successful push or a prerequisite -for ordinary publication. CI status, consumer retrieval, and hosted-page delivery -would remain separate concerns. There is no request for a combined publish-and- -deliver command, automatic tag cleanup, or a new release approval step. - -## Expected Benefit - -Promptkit could replace its copied Git query and comparison instructions with -one upstream-owned command and a short recovery link. Other ReleaseKit consumers -could use the same classifications and credential-safe evidence handoff, reducing -both duplicated instructions and divergent recovery behavior. diff --git a/docs/roadmap/publication-reconciliation.md b/docs/roadmap/publication-reconciliation.md deleted file mode 100644 index 6ccb047..0000000 --- a/docs/roadmap/publication-reconciliation.md +++ /dev/null @@ -1,350 +0,0 @@ -# Roadmap: read-only publication reconciliation - -Status: planned capability; storage, selection, credential handling, and output -decisions resolved. - -## Purpose - -Let maintainers revisit a failed or uncertain source-publication attempt through -one ReleaseKit operation. PromptKit currently copies the endpoint checks, exact -tag query, and result interpretation into its runbook. ReleaseKit should own -these mechanics and the handoff of original attempt evidence. - -The target includes a public Go API and a CLI command with equivalent behavior. -This roadmap describes the end state and required work, without selecting a -release version or prescribing a staged implementation plan. - -## Current baseline - -The current checkout contains the v0.7.0 release notes and publication workflow. -The [publication guide](../consumers/publish.md) describes existing outcomes; -the [maintainer runbook](../release.md#resolve-publication-failures) describes manual recovery. - -- `PublicationResult` supplies the repository and module identity, semantic - version, full tag, candidate commit, captured endpoint, tag object when known, - phase, and publication state. Its endpoint can contain credentials. -- Internal `reconcilePush` makes a bounded authoritative observation after a - failed push. It compares the annotated tag object and available peeled commit - evidence, returning published, absent, conflicting, or unknown outcomes. -- `internal/git.Endpoint.QueryTag` verifies the captured endpoint's scoped URL - resolution before querying the full tag and peeled ref. Inconsistent exact-ref - records remain unresolved. -- The temporary remote carries selected remote-specific transport settings, - including proxy, upload-pack, and server options, alongside the captured URL. -- The CLI masks HTTP(S) endpoint userinfo in diagnostics. The runbook still asks - maintainers to copy the displayed endpoint and compare remote object IDs. -- There is no public operation or CLI command for later reconciliation, and no - reusable CLI attempt record for preserving the exact transport endpoint. - -These existing observations and classifications provide the implementation -foundation. The main additions are a reusable operation, original-evidence -handoff, and documented operator and automation interfaces. - -## Targeted end state - -After connectivity or authentication is restored, a maintainer can give -ReleaseKit the original publication evidence and receive a fresh observation -of that attempt's destination and full tag. Library consumers use typed evidence; -CLI consumers use a record produced by ReleaseKit without parsing diagnostics. -Both Go API and CLI publication automatically retain attempt records through the -shared publication path. Recovery selects a stored record explicitly; Go callers -may also supply sufficient retained typed evidence directly. -Maintainers can discover saved attempts through a local, read-only listing when -the original publication output is unavailable. Discovery leaves record selection -explicit. - -| Observation | Reconciliation outcome | -| --- | --- | -| Intended annotated tag object, with consistent available commit evidence | Published | -| Authoritative full tag absent | Not published | -| Different tag object, including one pointing at the same commit | Conflict | -| Observation failure, inconsistent records, or insufficient original evidence | Unknown | - -The implementation preserves existing exact-object semantics, including current -handling of an unavailable peeled ref. A matching commit alone is insufficient. -Absence describes the current observation; it does not establish that the tag -never existed. A record identifies the intended attempt and is not proof that a -push happened or that the remote accepted it. - -Reconciliation uses the original captured endpoint and full tag, including nested -module prefixes. It checks that URL rewriting still permits observation of that -endpoint. A changed remote configuration does not substitute a new destination. -Authentication remains locally supplied through the supported Git mechanisms. -The original remote name is optional transport context. Later reconciliation -uses its current supported transport settings while keeping the recorded endpoint -fixed, allowing operators to repair local configuration before observation. - -The operation observes remote state without creating, pushing, moving, or -deleting tags, changing branches, validating a candidate, or delivering hosted -content. Current branch, worktree cleanliness, and current `HEAD` do not need to -match publication prerequisites. Repository context may still be used for Git -configuration and authentication. Corrective actions remain explicit. - -Successful source publication retains its existing completion point. Later -reconciliation is an optional recovery operation, with no routine read-back, -polling, CI, consumer-installation, or delivery gate. - -## Work needed - -### 1. Extract a shared observation and classification path - -**Rationale:** Immediate and later recovery should interpret the same evidence -consistently. - -**Target:** A reusable internal operation observes the captured endpoint and -classifies its exact tag using the existing publication semantics. - -**Work:** Separate remote observation and classification from the original push -error and publication orchestration. Reuse the scoped endpoint and exact-ref -query behavior. Keep immediate failed-push reconciliation bounded and preserve -its existing error context. Later reconciliation should honor caller cancellation -and return an unresolved outcome when observation cannot complete. It should not -invoke the current-remote equality guards that belong to an active publication. -Read the supported transport settings using the recorded remote name when -available, and apply them to the temporary remote with the recorded endpoint. -Keep the endpoint-resolution check. Missing remote settings permit observation -with available endpoint/global configuration; configuration-read failures remain -unresolved with their causes. Distinguish these cases from a changed destination. - -### 2. Define public evidence and reconciliation results - -**Rationale:** Consumers need supported types for recovery instead of rebuilding -internal Git behavior or parsing diagnostic strings. - -**Target:** A root-facade operation, provisionally `ReconcilePublication`, accepts -original evidence and returns a useful observation with preserved error causes. - -**Work:** Define the minimum evidence needed for each classification: captured -endpoint, full tag, intended tag object, and candidate commit as applicable. -Carry module/version information for operator context. Reuse existing publication -state meanings while distinguishing the original attempt from the new observation. -Define behavior for incomplete evidence and unsupported record formats without -inferring missing identity from today's remote, tag, or profile. A deleted or -moved local tag should not invalidate otherwise sufficient retained evidence. -Expose the saved record location and any recording failure separately from the -publication outcome. Accept retained typed evidence directly for reconciliation -so observation does not depend on filesystem persistence. -Return a single facade-owned evidence value from publication that reconciliation -accepts directly. Provide a supported record-loading helper returning that same -type so Go consumers can recover without copying fields or implementing JSON -parsing. -Include the original remote name as optional context in typed evidence and saved -records. Recovery uses current connection settings rather than a stored snapshot -of transport configuration; the recorded endpoint retains its identity role. - -Keep facade ownership and internal Git boundaries. Exact type names, field names, -and signatures belong in API design; no new public implementation package is -needed for this feature. - -### 3. Provide a practical attempt-evidence handoff - -**Rationale:** Redacted diagnostics are useful displays but cannot reliably carry -the original transport URL into a later process. - -**Target:** Shared publication automatically retains a small versioned record -for both Go API and CLI consumers. Library callers receive typed evidence, -record location, and recording-failure information; the CLI presents that result. - -**Work:** Capture evidence when the endpoint and tag object are established, -before attempting the push. Preserve useful partial evidence if an earlier stage -fails, clearly identifying what is missing. Make record production and loading -available through supported interfaces. Keep original identity separate from -later observations and avoid replacing one attempt's evidence with another's. -Implement automatic private storage with an optional path override and the -sensitive-data handling recorded below. Use ordinary file handling proportionate -to recovery; this feature does not require a transactional journal or power-loss -guarantees. Keep recording inside the shared publication path so library callers -receive the same default and optional path override without consumer-owned hooks. - -### 4. Expose recovery through the CLI and documentation - -**Rationale:** A single command should replace copied query and comparison steps. - -**Target:** A command provisionally named `reconcile` reads an explicitly selected -attempt record and reports the outcome, selected full tag, intended identity, -and actionable cause context. Automation has a documented structured result and -exit behavior. - -**Work:** Map the command to the facade, retaining credential-safe displays and -errors. Document valid invocation, missing or unreadable records, unknown -observations, and cancellation. Replace the runbook's manual endpoint-copy -procedure with this workflow and a concise link from consumer guidance. Explain -how maintainers retain records and deliberately address absence or conflicts. -Keep the distinction between source publication and hosted delivery explicit. -Document recovery limits for older attempts that lack sufficient saved evidence. -During publication, print a copyable recovery command when a saved record is -available and recovery is relevant. Include the explicit record path, with -appropriate argument quoting, and preserve credential-safe output. Select the -record supplied by the caller rather than inferring the latest attempt. - -Add a local, read-only attempt listing for the default or explicitly selected -storage location. Show each record's path, module, full tag, candidate commit, -and recording time when available, with credentials redacted. Record the time -as discovery metadata; it does not establish whether a push occurred. Listing -reads saved records without querying remotes or choosing an attempt for recovery. -Include discovery in the recovery guide for interrupted processes and lost output. -Return readable attempts alongside per-record diagnostics for unreadable, -malformed, or unsupported-format records. Identify the affected paths and retain -credential redaction. Expose entries and diagnostics to Go callers and structured -output; the CLI shows usable entries and indicates incomplete discovery. Report -failure to enumerate the storage location as a discovery failure, not an empty -collection. An explicitly selected unreadable or unsupported record produces its -own error without selecting a different attempt. - -Make unresolved diagnostics identify missing evidence, unreadable records, -endpoint rewriting, and failed remote observation distinctly. Identify the -specific missing information or configuration issue when known and explain the -next useful operator action. For conflicts, show expected and observed tag-object -IDs. Preserve cause information without guessing whether a transport failure is -an authentication or connectivity problem. Apply these diagnostics to human and -structured output while leaving corrective actions explicit. - -### 5. Verify recovery behavior and integration - -**Rationale:** Incorrect recovery can direct corrective action at the wrong -repository or mistake a different annotated tag for the intended release. - -**Target:** Behavioral coverage establishes equivalent immediate/later -classification and useful recovery across process boundaries. - -**Work:** Use temporary Git repositories and local remotes to cover exact matches, -absence, different objects at the same commit, root and nested tags, changed -current remotes, rewriting that redirects the captured endpoint, inconsistent -observations, unavailable remotes, and cancellation. Test record round trips, -partial evidence, default and overridden storage paths, storage failures, -private storage behavior, and redaction with synthetic credentials. Cover automatic -recording through both API and CLI publication, direct typed-evidence recovery, -and explicit selection among records for multiple modules and retries. Verify -that recording failure remains separate from publication outcome and that the -printed recovery command selects its original record. Exercise -recording before a failed or interrupted push and a later -invocation after local branch or tag state changes. Verify the diagnostic -operation performs no publication or cleanup actions and runs no candidate tests. -Reuse existing tests for unchanged Git behavior rather than duplicating them. -Verify that publication and record loading yield evidence directly usable by the -Go reconciliation API. Cover discovery across modules and retries, default and -overridden storage locations, partial records, and credential redaction. Verify -that listing performs no remote observation or automatic attempt selection. -Cover the diagnostic distinctions and expected/observed conflict identities -without requiring incidental error wording. -Cover current remote-specific transport settings, repaired configuration, -missing or renamed remotes, and configuration-read failures. Verify that changed -fetch or push URLs do not replace the recorded endpoint and that endpoint -rewriting still receives the existing check. Exercise discovery containing both -readable and damaged or unsupported records, storage-enumeration failure, and -explicit selection of an unusable record. Preserve useful partial discovery -results and credential-safe diagnostics across the API and CLI. - -Run the normal offline suite and facade checks when code lands. Update GoDoc, -CLI documentation, the publication guide, and recovery instructions together. -Any release-policy clarification should describe later optional recovery without -adding checks to successful publication. This roadmap itself changes no code. - -## Resolved decisions - -### 1. Automatic storage with an optional path override - -Retain records automatically in private repository-local Git metadata by default, -outside the tracked worktree, with a distinct record for each attempt. The -standard location should serve nearly all consumers without configuration. -Provide an optional storage-path override for consumers with particular needs. -Apply the same record handling and privacy behavior at either location; exact -configuration syntax belongs in interface design. - -Automatic recording belongs in the shared publication path for both Go API and -CLI callers. Publication results expose the saved record location and any -recording failure separately from the publication outcome. The library returns -this information and the CLI presents it. - -Report the record path and require explicit selection for stored-record -reconciliation. Record evidence before the push once the necessary identity -exists. Treat storage failure as a clearly reported loss of the saved handoff, -while preserving the actual publication result and existing publication -prerequisites. An override -changes the location, not publication's response to a recording failure. - -Automatic storage removes a routine setup decision and keeps default records -out of commits. Records accumulate locally, and recovery evidence can be lost -when storage fails or its location is removed. Document retention and manual -removal, including the sensitive nature of records at overridden locations. -Automatic cleanup is not required for the initial capability. - -### 2. Preserve the exact endpoint in private records - -Preserve the exact captured endpoint in the private record, with owner-only -access using platform-supported file permissions. Continue redacting endpoint -credentials in human and machine diagnostic output; expose only the record -location for the handoff. Prefer locally supplied Git credentials in adoption -guidance. Treat exports and copies of the record as potentially sensitive, -just like the existing API result's endpoint. - -This supports existing publication configurations without reconstructing an -endpoint from its display. It also places potentially sensitive material on disk; -permissions and clear documentation reduce accidental exposure but do not promise -encrypted storage or protection from other processes running as the same user. - -### 3. Human-readable and structured outcomes - -Offer human-readable output by default and a structured mode with redacted -identity, reconciliation state, and cause information. Use success for confirmed -publication, a non-success classification for absence/conflict, and distinct -unknown and invocation-error classifications, following the existing CLI -conventions where practical. Define exact numeric codes during CLI design. - -This makes shell recovery straightforward and keeps structured state authoritative -for detailed consumers. A successful observation of absence still yields a -non-success process status; document that distinction explicitly. - -### 4. Explicit attempt selection - -Reconciliation of a stored attempt requires an explicit record path. This keeps -selection unambiguous across modules, retries, and changed repository state. -When a record is available and recovery is relevant, publication prints a -copyable command selecting that record. Automatic selection of the latest record -is outside this workflow. - -Go callers may instead reconcile sufficient retained typed evidence directly. -Explicit stored-record selection does not make filesystem storage a prerequisite -for the observation API. - -### 5. Recovery ergonomics - -Provide local attempt discovery to recover the record path after an interrupted -process or lost publication output. Listing exposes useful, credential-safe -identity and recording metadata while preserving explicit selection. - -Publication returns one reusable typed evidence value. Reconciliation accepts -that value directly, and the supported record loader returns the same type. -These interfaces keep evidence transfer and serialization mechanics in ReleaseKit. - -Diagnostics distinguish missing evidence, unreadable records, endpoint rewriting, -and failed remote observation, and identify useful next actions when known. -Conflict output includes expected and observed tag-object IDs. This makes -recovery actionable while preserving the existing outcome classifications and -explicit corrective-action workflow. - -### 6. Current transport settings with a fixed recorded endpoint - -Record the original remote name as optional transport context. During later -reconciliation, read its current supported transport settings and use them with -the exact recorded endpoint. The remote's current fetch and push URLs do not -replace that endpoint. Retain the existing check that URL rewriting leaves the -recorded endpoint unchanged. - -A missing or renamed remote alone does not invalidate the evidence. Observation -can use available endpoint/global configuration; operators may restore required -settings locally. Configuration-read failures and failed observations remain -unresolved with cause information. This separates original publication identity -from repairable connection settings without snapshotting transport configuration. - -### 7. Discovery preserves readable attempts - -An unreadable, malformed, or unsupported record produces a diagnostic while -other readable attempts remain discoverable. Results expose both entries and -per-record diagnostics, identifying affected paths with credential-safe output. -The CLI presents usable entries and makes incomplete discovery clear. - -Failure to enumerate storage is a discovery failure rather than an empty result. -Explicit reconciliation of an unusable record reports that record's error and -retains the caller's selection. Discovery and record errors do not authorize -automatic selection or corrective actions. diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md deleted file mode 100644 index 888327b..0000000 --- a/docs/roadmap/remediation.md +++ /dev/null @@ -1,802 +0,0 @@ -# Publication reconciliation remediation - -Status: complete. R01–R17 are implemented and verified. The full offline suite -and compiled facade/dependency checks passed with Go 1.26.8 after the follow-up -corrections. R01–R13 retain their original completion evidence; R14–R17 describe -the subsequent stage-by-stage review of `363f7cd` and its fixes. - -R01–R13 describe commits `4dff808` through `599ad81`, against planning baseline -`447b42a`. Their audit evidence and defect descriptions refer to that reviewed -baseline; each completion marker records the implemented correction. The full -offline suite (`go test ./...`) and compiled facade/dependency checks -(`go run ./internal/apicheck`) passed with Go 1.26.8 after that remediation. - -The [feature roadmap](publication-reconciliation.md) defines the requested -behavior; the [implementation plan](implementation.md) supplies the intended -implementation. The broad package structure is in place: the root facade owns -public evidence and recovery types, `internal/publish` owns observation, -recording, and discovery, `internal/git` owns scoped endpoints and ref queries, -and the CLI owns command parsing, presentation, and exit classification. -Each finding records its reviewed defect, implemented correction, and -verification. - -## Review evidence - -The full offline suite (`go test ./...`) and compiled facade/dependency checks -(`go run ./internal/apicheck`) passed with Go 1.26.8. Additional disposable probes -used local Git repositories, synthetic credentials, controlled Git subprocesses, -and failing local output. They reproduced the findings below without changing -production code or adding repository tests. Passing existing tests does not -cover these failure combinations. - -## R01: Preserve candidate failures and keep storage failures separate — P1 - -**Completed:** Capture and storage errors now remain separate. Focused publication -regressions passed for invalid/missing notes, retained Git causes, saved partial -evidence, and successful publication after storage lookup failure. - -**Relevant code:** `internal/publish/publish.go:110`, especially the assignment -at line 120 and the error check at line 127. - -`candidate.Capture` can return useful repository/identity fields together with an -error, such as a missing or invalid release note. When its repository field is -present, the new storage initialization assigns `resolveRecordDir`'s error to -the same `err` variable before the capture error is checked. - -This produces two opposite defects: - -- Successful storage resolution erases the capture error. Publication continues - with a candidate that failed capture. The probe used a release note with the - wrong product heading: `Check` rejected it, while `Publish` ran the product - callback, returned `published` with a nil error, and pushed the annotated tag - to the local remote. -- Failed storage resolution replaces a successful capture with a publication - failure. A controlled failure of the Git common-directory lookup returned - `not_attempted`, ran no product callback, and returned a `capture release - candidate` error as well as `RecordError`. - -**Expected behavior:** A capture error stops publication while preserving useful -partial evidence. A recording/storage error is reported separately and leaves -the publication result governed by its existing checks and push outcome. - -**Remediation:** Keep capture and storage errors in separate variables. Preserve -the original capture failure across record initialization/finalization; use -`RecordError` exclusively for recording failures. Add regressions for invalid -and missing notes, capture-error cause preservation, and a valid candidate whose -storage-directory lookup fails. Verify that rejected candidates run no product -checks or tag creation, and that storage lookup failure permits an otherwise -successful publication. - -## R02: Validate the filename that will actually be written — P2 - -**Completed:** Each generated filename is checked before that exact path is -created, including collision retries. Deterministic selective-ignore tests passed -and confirm that rejected writes leave the candidate clean and existing records -unchanged. - -**Relevant code:** `internal/publish/record.go:68`, `record.go:85`, and -`record.go:134`; the pre-push save in `internal/publish/publish.go`. - -The override guard generates a random proposed filename and checks its Git -ignore status. The creation loop then generates a different random filename -and writes that file without checking its ignore status. - -Filename-dependent ignore rules therefore allow the check and actual write to -disagree. A probe using `selective/attempt-[0-7]*.json` in `.gitignore` successfully -saved a record starting with `attempt-c...`; Git reported that actual record as -untracked. The save returned success. The reverse mismatch can also reject a -name even though another name the writer would choose is ignored. - -**Expected behavior:** The storage safeguard applies to the exact path passed to -exclusive creation. A failed storage choice leaves the handoff unavailable -without introducing unignored candidate contents. - -**Consequences:** During publication, saving occurs after the final clean-tree -guard. An unchecked actual path can therefore leave the worktree dirty while -publication succeeds, and can expose a sensitive record to a later ordinary -`git add` operation. The already captured release tag does not itself include -that new file. - -**Remediation:** Generate one candidate path, validate that path, and use the -same path for exclusive creation. Repeat that sequence if a filename collision -requires another candidate. Add deterministic coverage with filename-dependent -ignore rules; verify that successful writes are ignored and rejected choices -leave the worktree clean. A naming seam can make this test deterministic rather -than relying on random retries. - -## R03: Allow an explicit override into Git metadata — P2 - -**Completed:** The override guard recognizes Git-discovered private and shared -metadata directories. Recording tests passed for ordinary, linked-worktree, and -separate-Git-directory layouts, alongside rejection of unignored worktree files. - -**Relevant code:** `internal/publish/record.go:142`, especially the worktree -containment and `git check-ignore` handling at lines 148–159. - -For a conventional checkout, the default store is inside -`.git/releasekit/publication-attempts`. Selecting that same directory explicitly -activates the override guard. It treats the path as worktree content and demands -an ignore-rule match, although Git metadata is already excluded from tracked -source and ordinary status reporting. - -The probe saved a default record successfully, then selected its parent -directory as the explicit override. The second save failed with `record_write` -and an assertion that the metadata path was inside the worktree and unignored. - -**Expected behavior:** The agreed storage choices include Git metadata, -external locations, and Git-ignored worktree locations. An override changes the -location rather than imposing a requirement to add ignore rules for metadata. -The rejection is an additional storage prohibition beyond the intended policy. - -**Remediation:** Identify Git metadata through Git's directory discovery and -recognize that it is outside candidate contents, including linked-worktree and -separate-Git-directory layouts. Apply ordinary ignore checks to actual -worktree-content paths. Add coverage for explicitly selecting the default store -and other private locations in Git metadata, retaining rejection of genuine -unignored worktree contents. - -## R04: Restore cancellation across storage and recovery phases — P2 - -**Completed:** Storage and discovery share context-aware Git adapters; later -observation classifies canceled tag validation and queries consistently. Tests -passed for already canceled discovery and cancellation synchronized with active -root/metadata lookups, ignore checks, publication storage, and tag operations. -Focused Git, facade, and CLI recovery checks also passed; immediate failed-push -reconciliation retains its existing bounded context. - -**Relevant code:** `internal/publish/record.go:112` and `record.go:152`; -`internal/publish/attempts.go:31` and `attempts.go:74`; -`internal/publish/reconcile.go:109` and `reconcile.go:130`. - -Cancellation behavior depends on which phase is active: - -- `resolveRecordDir` and the ignore guard use `exec.Command` without the caller's - context. A probe held the common-directory lookup, canceled publication, and - observed that the call remained blocked until the controlled Git command was - explicitly released. -- Discovery resolves a default/relative store before checking `ctx.Err()`, and - wraps all resolution failures as `storage_enumeration`. An already canceled - default-store request consequently returned that kind instead of `canceled`. -- `recordDirectoryFor` uses raw `exec.CommandContext`, which can return only a - killed-process error when cancellation occurs during Git execution. The - active-discovery probe returned `storage_enumeration` and - `errors.Is(err, context.Canceled) == false`. -- Later observation checks cancellation in some branches but not the tag-ref - validation and `QueryTag` error branches. Canceling an active tag query - returned `remote_observation`, even though its error chain correctly retained - `context.Canceled`. - -**Expected behavior:** Caller cancellation stops cancellable Git work and is -reported as `canceled`, with the context cause retained. Discovery preserves -already obtained entries. Immediate failed-push reconciliation retains its -separately selected bounded recovery context. - -**Remediation:** Consolidate the duplicated storage-directory/Git command work -behind the shared internal Git/process boundary, passing the relevant context -and preserving its cause. Normalize recovery errors at each failed operation -using the known context cause, rather than assigning a phase kind first and -losing cancellation. Keep partial evidence finalization and recording warnings -separate from the original publication failure. Add coordinated tests for -already canceled and actively canceled default/relative discovery, blocked -storage Git commands, and cancellation during tag validation/query. Preserve -the existing immediate-recovery timeout semantics. - -## R05: Redact recovery paths and diagnostic fields at display boundaries — P2 - -**Completed:** Facade errors and CLI recovery displays now redact recognized -HTTP credentials in paths, metadata, diagnostics, and handoff commands. Public -error and CLI human/JSON regressions passed; raw typed causes and private record -contents remain intact. - -**Relevant code:** `recovery.go:53`; `cmd/releasekit/recovery.go:76`, its JSON DTO -construction, and human path displays; `cmd/releasekit/main.go`'s record displays. - -`PublicationRecoveryError.Error` redacts its wrapped cause but interpolates its -own `RecordPath` directly. The CLI also copies record paths and diagnostic -messages directly into human and JSON output. Safe evidence formatting protects -the endpoint field, but does not provide a general safe-display boundary for -these other strings. - -Passing the synthetic value -`https://audit-user:audit-secret@example.invalid/attempt.json` to -`LoadPublicationEvidence` reproduced the issue. The nested file-read cause was -redacted, while the facade error's path still contained `audit-secret`. -`reconcile --record` with the same invalid local-record selection also emitted -the synthetic credential in JSON output. This can expose a credential-bearing -URL pasted into the wrong argument, despite the documented credential-safe -error and diagnostic behavior. - -**Expected behavior:** Recognized endpoint credentials are redacted throughout -human and structured diagnostics. Raw values remain available in typed evidence, -private records, and the filesystem arguments used for the actual operation. - -**Remediation:** Apply the existing diagnostic/URL formatter to every relevant -display string, including recovery-error paths and CLI path/identity/diagnostic -fields, before formatting or JSON encoding. Keep raw operation inputs and private -serialization intact. Add public-error and CLI human/JSON regressions covering -synthetic credential-bearing paths, causes, and metadata, alongside the existing -endpoint-only case. No new credential-storage policy is needed. - -## R06: Propagate structured-output write failures — P2 - -**Completed:** Both recovery commands now report JSON encoder failures on -standard error and return status 1. Failing-writer regressions passed alongside -ordinary recovery status tests; observed publication state remains unchanged. -The CLI reference records the reporting-failure status. - -**Relevant code:** `cmd/releasekit/recovery.go:242` and its two `writeJSON` -call sites. - -`writeJSON` explicitly discards the error returned by `json.Encoder.Encode`. -Command status is then chosen entirely from observation/discovery results, -even when no structured result reaches the caller. - -The probe enumerated an empty existing store with `--json`, directing stdout -to `/dev/full`. It returned status 0 with no stderr diagnostic despite the -failed write. The same helper is used by successful reconciliation. - -**Expected behavior:** Automation can distinguish successful output delivery -from an output failure. A local reporting failure remains separate from the -already observed remote publication state. - -**Remediation:** Return and handle the encoder error. Report the local output -failure on stderr and return an appropriate operational failure status, while -preserving the actual observation in the implementation rather than converting -it to unknown remote state. Document that output-failure status in the CLI -contract. Add focused failing-writer coverage for both structured commands; -ordinary writable-output state/status behavior should stay unchanged. - -## Integration audit evidence - -A subsequent audit examined the interaction with existing consumer launchers, -repository discovery, candidate cleanliness, and the maintainer runbook. The -production code remains identical to the code covered by the full-suite/API -checks above. A fresh focused run of the existing launcher, Woodpecker, -publication-handoff, and lost-output recovery integration tests passed with -Go 1.26.8. Disposable probes reproduced the additional issues below. R01–R06 -remain open and are preserved rather than duplicated here. - -## R07: Make the recovery handoff survive a cold Go-tool invocation — P2 - -**Completed:** CLI recovery prefers the selected module's Go tool. API-only -consumers receive privately retained alternate module/checksum files under Git -metadata; native consumers retain the executable-path fallback. Assembled tests -passed after cold executables disappeared and launcher files were removed, -covering both adoption styles, relative replacements, clean candidates, and -recovery exit statuses 0–3. CLI, consumer, and internal guidance is updated. - -**Relevant code:** `cmd/releasekit/main.go:134` (`publicationRecoveryCommand`); -`examples/multi-module/release.sh`; `docs/consumers/adoption.md`'s module-selected -Go-tool workflow; `cmd/releasekit/publication_handoff_test.go`. - -The new handoff always uses `os.Executable()` as the command to run later. -Existing consumer guidance and the maintained launcher execute ReleaseKit -through `go tool releasekit`. On an uncached tool build, Go 1.26.8 executes a -temporary build executable and removes it when the Go driver exits. A warm -invocation uses a surviving cached executable, so its behavior differs. - -**Evidence:** A disposable tool-registered consumer used the current CLI with a -cold tool build and a local remote that rejected the tag push. Publication -returned status 1 and saved a complete record, but its printed recovery command -named `/tmp/go-build.../exe/releasekit`. That executable was already absent when -the driver returned, and copying the command returned status 127. A surviving -CLI reconciled the same record successfully to `not_published` with status 1. -The existing handoff test builds a persistent executable directly and therefore -does not exercise this launch lifecycle. - -**Expected behavior:** The copyable handoff works with supported consumer launch -methods after the publication process has exited. Routine first use on a fresh -machine/cache should not require reconstructing another invocation manually. -The existing fallback guidance for a subsequently unavailable executable does -not supply a working handoff for this ordinary cold-build case. - -**Decision:** Prefer a module-selected Go invocation for recovery where the -consumer setup supports it. Provide an explicit, durable recovery path for -API-only consumers using temporary tool declarations. ReleaseKit owns the -handoff mechanics and supports both adoption styles. This follows the existing -tool-selection workflow and keeps routine binary lifecycle management out of -consumer code. - -**Remediation:** Make the recovery invocation independent of an ephemeral -Go-tool executable using the selected module-based approach and an explicit -API-only recovery path. Account for the maintained launcher's temporary -`-modfile` cleanup and its support for ordinary -package dependencies as well as registered tools; a bare `go tool` command -cannot assume every consumer has a permanent tool declaration. Keep explicit -record selection and distinct exit statuses. Add an assembled cold-build test -that executes the printed command after the original Go driver and launcher -have exited, rather than only testing a directly built CLI or a warm cache. - -## R08: Reuse the existing repository-path semantics for record storage — P2 - -**Completed:** R04's shared resolver reuses the established Git root and -metadata-path normalization. Added regressions passed for repository roots and -separate metadata directories ending in whitespace, default/relative read-write -agreement, and rejection of absolute unignored worktree stores. Existing ordinary -and linked-worktree recording tests also passed. - -**Relevant code:** `internal/publish/record.go:112`, especially line 117; -`internal/publish/attempts.go:74`, especially line 82; the existing -`internal/git/source.go:39` and `source.go:50` repository-root helpers. - -The pre-existing root resolver removes Git's output newline and resolves -filesystem aliases, preserving valid whitespace in the path itself. The new -recording and discovery resolvers independently use `strings.TrimSpace`, which -changes repository identity when a directory name ends in whitespace. Recording -then uses that altered root both for relative overrides and for deciding whether -an absolute override lies inside the candidate worktree. - -**Evidence:** In a temporary checkout whose final directory component was -`consumer `, the existing `git.RepositoryRoot` preserved the exact path and -the existing candidate `Check` completed successfully. The new code produced -three inconsistent results: - -- A relative `.releasekit/attempts` override tried to use the different sibling - path under `consumer` and failed its Git ignore check, despite the real - `.releasekit/` location being ignored. -- After a record was saved in the correct absolute ignored location, relative - discovery selected the wrong sibling directory and returned a successful - empty collection. The actual record still existed in the requested store. -- An absolute override inside the actual worktree was misclassified as - external because containment used the trimmed root. It successfully wrote an - unignored record, which Git reported as untracked. - -**Expected behavior:** Publication, discovery, relative overrides, and worktree -containment use the same repository identity already supported by the library. -Path data remains intact. This failure is independent of R02's filename mismatch: -the containment decision itself uses the wrong root. - -**Remediation:** Reuse `internal/git.RepositoryRoot` and share metadata-path -resolution through the Git boundary, coordinating with R04 rather than adding -another resolver. Remove output terminators without stripping path characters. -Apply the same preservation to common-directory output. Add focused regressions -for a valid root ending in whitespace, relative-store read/write agreement, and -absolute unignored-path rejection. Preserve existing filesystem-alias and -linked-worktree behavior instead of introducing new pathname restrictions. - -## R09: Align maintainer recovery instructions with the source bootstrap — P2 - -**Completed:** Runbook discovery and reconciliation use the prepared source-built -CLI, and the roadmap recovery link targets the current heading. Disposable local -checks executed both snippets with no global `releasekit`, confirming discovery -and the expected incomplete-evidence reconciliation status. - -**Relevant code/docs:** `scripts/release:9`; `docs/release.md:104` and -`release.md:149`; `docs/roadmap/publication-reconciliation.md:21`. - -The existing maintainer procedure builds and executes -`.releasekit/bin/releasekit`. It does not install an executable named -`releasekit` on `PATH`. The new lost-output recovery instructions instead run -`releasekit publication-attempts` and `releasekit reconcile` without identifying -the executable that the procedure prepared. - -**Evidence:** With Go and Git available on `PATH` and only the source-built CLI -in `.releasekit/bin`, the newly documented discovery command returned 127 -(`releasekit: not found`). The equivalent command using -`./.releasekit/bin/releasekit` returned 0 and listed the saved attempt. A globally -installed CLI could also be a different version from the source-built one. - -The recovery-section rename additionally left the feature roadmap's link to -`docs/release.md#resolve-failures` pointing at a heading that no longer exists. - -**Expected behavior:** The repository's executable runbook uses the CLI prepared -by its own prerequisites, and its recovery links resolve to the current owner. -It requires no additional global installation for lost-output recovery. - -**Remediation:** Use the source-built executable for discovery and reconciliation -in the maintainer runbook, consistent with its existing delivery commands. -Retain explicit instructions for selecting another supported CLI when the -original executable is genuinely unavailable. Repair the renamed-section link. -Verify the recovery snippets in an environment without a global `releasekit` -executable; keep generic consumer command examples in their consumer-facing -documentation. - -## R10: Preserve endpoint bytes across the private-record handoff — P2 - -**Completed:** Private records retain ordinary version-1 JSON and select a -version-2 base64 byte representation for non-UTF-8 fields. Both versions load; -unknown fields remain ignorable and malformed known byte fields report format -errors. Round-trip tests passed for all evidence fields, and real Git -observations distinguish the original byte path from a replacement-character -decoy consistently before and after loading. The record-format guide is updated. - -**Relevant code:** `internal/publish/record.go:77` and its evidence decoding; -the string fields in `internal/publish/reconcile.go`'s `Evidence`; the existing -Git endpoint resolution/query path in `internal/git/remote.go` and `tag.go`. - -On POSIX, the existing Git transport can use local repository paths containing -bytes that are not valid UTF-8. Typed evidence retains those bytes in its Go -strings. The new private JSON codec uses `json.Marshal` directly on those -strings, which replaces invalid UTF-8 with the Unicode replacement character -without reporting an error. Loading the record therefore supplies a different -endpoint to the existing Git observer. - -**Evidence:** A disposable valid candidate's publication attempt targeted a local -repository whose path contained a non-UTF-8 byte. Its hook rejected the push; -publication returned `not_published` with a complete saved record and the exact -original endpoint in typed evidence. A second local repository, named using the replacement-character -form of that path, held the same intended annotated tag object. Direct typed -reconciliation correctly returned `not_published` at the original destination. -Loading the saved record changed its endpoint to the second repository and -record-based reconciliation returned `published` there. The record save/load -reported no error. - -**Expected behavior:** Direct and record-loaded evidence identify the same -captured destination. The private handoff retains exact transport identity, -including pathname bytes supported by the existing Git boundary, or reports -that the handoff could not be saved faithfully. Diagnostic JSON is a display -format and can use safe displays; the private record supplies actual operation -inputs and must preserve their identity. - -**Decision:** Use a lossless private-record representation for supported -pathname and endpoint bytes. This preserves existing publication inputs and -keeps record-loaded recovery consistent with direct typed evidence. Recording -failures remain separate from the publication outcome. - -**Remediation:** Make private serialization lossless for supported evidence, -using an explicit byte representation where needed. Keep the original typed -evidence and publication behavior intact; avoid adding a general UTF-8 -restriction to previously valid Git publication inputs. Cover endpoint/path -round trips and equivalent direct-versus-loaded observations, including a -distinct replacement-character path, in focused regression tests. - -## Algorithmic audit scope and evidence - -This audit retains the planned flat record store, complete local discovery, -deterministic ordering of selected record paths, versioned JSON format, and -exact-tag observation semantics. It does not propose indexes, pagination, -automatic retention, or changes to those selected algorithms. - -No unexpected quadratic algorithm was found in the added production code. -Classification operates on a fixed number of fields and bounded-length object -IDs. Record encoding/decoding uses a fixed number of linear passes over a -record. Facade and CLI result conversions are linear in returned entries and -displayed data. Full-result discovery necessarily retains the records its API -returns. The refinements below concern work outside those necessary operations -and one partial-result correctness defect. - -The existing codec, discovery, and classification tests passed in a fresh -focused run with Go 1.26.8. Complexity analysis used the implementation and -the local Go standard-library source, without benchmarks or real-world -performance measurements. Disposable functional probes confirmed the parser -cause loss and partial-enumeration result loss. The latter used a temporary -source overlay only to make the directory-reader boundary return real entries -with a controlled error; repository production and test files were unchanged. - -## R11: Filter directory entries before ordering them — P3 - -**Completed:** Discovery filters unsorted directory batches and sorts selected -record paths once. Directory work is `O(D + R log R)` with bounded intermediate -entry storage. Mixed-directory, ordering, and cancellation regressions passed. -R13 addresses preservation of records alongside enumeration failures. - -**Relevant code:** `internal/publish/attempts.go:41` through the subsequent -filter and `sort.Strings(paths)`. - -`os.ReadDir` already sorts every directory entry by filename. Discovery then -filters for JSON record files and sorts their full paths again. Consequently, -an overridden store containing many unrelated files pays to order names that -will never appear in the result. This is an implementation choice beyond the -planned requirement to order selected records. - -**Complexity:** Let `D` be all directory entries, `R` the selected JSON file -candidates, and `B` their total bytes read. Treating name comparisons as unit -operations, the current directory ordering has worst-case `O(D log D)` cost -before the same `O(B)` record work. Filtering an unsorted enumeration and sorting -only selected paths costs `O(D + R log R + B)`. When `R` is bounded and `D` -grows, directory processing improves from `O(D log D)` to `O(D)`. When nearly -every entry is a record, the planned sorting bound remains unchanged. The -second sort currently receives already ordered paths; it is redundant, rather -than evidence of a quadratic algorithm. - -**Expected behavior:** Keep complete discovery and deterministic path ordering -for attempts and their diagnostics. Unrelated directory entries need no order. - -**Remediation opportunity:** Use the standard `os.File.ReadDir` interface to -enumerate without implicit sorting, filter candidates, and order those paths -once. Batched reads can also reduce intermediate directory-entry storage from -`O(D)` to `O(batch size + R)`, excluding retained result data. Preserve partial -enumeration errors as described in R13 and cancellation behavior from R04. -Keep this a local enumeration refinement; no public file-count limits or -changes to record selection are required. - -## R12: Reject trailing JSON without constructing a discarded value — P3 - -**Completed:** Loading uses complete-document unmarshalling and wraps the actual -parser error. It avoids constructing a discarded trailing value while retaining -the whole-file read bound. Tests passed for single documents, whitespace, -additional fields, extra documents, malformed suffixes, and retained syntax -causes, alongside byte-preserving record regressions. - -**Relevant code:** `internal/publish/record.go:24`, particularly the second -`decoder.Decode(&trailing)` and its error handling at lines 34–37. - -After decoding the envelope, the loader decodes another complete JSON value -into `any` solely to check whether one exists. A second value always makes the -record invalid. Nevertheless, an appended array or object is fully parsed and -materialized before being discarded. Only trailing whitespace and EOF are -acceptable at that point. - -**Complexity:** For a first envelope of `L` bytes and an appended value of `T` -bytes containing `M` nodes, the second decode adds `Theta(T)` parsing work and -up to `Theta(M)` object/array storage. Rejecting at the first non-whitespace byte -after the envelope needs no second value tree. A standard-library complete- -document unmarshal of the already loaded bytes can reject that trailing input -during syntax validation before constructing it. This removes avoidable parsing -and allocations; the existing whole-file read still has its `Theta(L + T)` I/O -and buffer bound. It is not an overall quadratic-to-linear claim. - -There is also a correctness inconsistency: a non-EOF error from that second -decode is replaced by a new message without wrapping the original error. -Functional probes showed that an incomplete first document retained -`io.ErrUnexpectedEOF`, while an incomplete trailing document lost it. A syntax -error in the first document was available through `errors.As` as -`*json.SyntaxError`; the corresponding trailing syntax error was not. - -**Expected behavior:** Require one complete document, retain supported partial -evidence/additional-field behavior, and preserve available parsing causes in -the typed `record_format` error. Invalid extra data needs no materialized tree. - -**Remediation opportunity:** Simplify loading to a complete-document unmarshal -into the versioned envelope, retaining existing format/evidence validation and -cause wrapping. An equivalent trailing-data check can also work if it avoids -decoding an arbitrary second value and retains real parser failures. Add focused -single-document, extra-document, trailing-whitespace, and malformed-suffix -coverage. Keep raw record identity handling coordinated with R10; no new size -caps or public input restrictions are needed. - -## R13: Retain entries returned with a directory-enumeration error — P2 - -**Completed:** Discovery loads returned record paths before reporting enumeration -failure. Fault-injected entries-plus-error and zero-entry tests passed, preserving -readable records, per-record diagnostics, ordering, and the original cause. -Facade conversion and CLI human/JSON tests confirm partial results survive with -a non-success status. The full offline suite and API/dependency checks passed. - -**Relevant code:** `internal/publish/attempts.go:41` through its immediate error -return at lines 42–44; Stage 5's partial-result requirement in the implementation -plan. - -Go's `os.ReadDir` can return both entries read successfully and a later read -error. Discovery checks the error first and immediately returns its still-empty -result, dropping every returned entry. Thus a filesystem enumeration failure -can hide readable attempts that were already discovered, even though the API -and CLI support returning attempts alongside a top-level failure. - -**Evidence:** A disposable probe supplied the directory-reader boundary with -the real entry for one valid record plus a controlled enumeration error, an -outcome permitted by `os.ReadDir`'s contract. `ListPublicationAttempts` returned -zero attempts with `storage_enumeration`. Without the injected error, it -returned that same record successfully. This was fault injection at the read -boundary, not a change to the discovery algorithm under review. - -**Expected behavior:** Preserve readable attempts and per-record diagnostics -from returned entries while reporting the top-level enumeration failure. The -operation remains incomplete and retains its non-success CLI classification. - -**Complexity:** Processing a returned prefix of `d` entries containing `r` -record candidates and `b` readable bytes takes the same ordinary filtering, -ordering, and decoding work: `O(d + r log r + b)` with R11's refinement. The -existing `os.ReadDir` approach instead retains its `O(d log d + b)` bound. -Neither correction requires another directory scan: both retain the names -already enumerated and perform the normal record loads for those names. - -**Remediation:** Retain the enumeration error, process available entries using -the normal discovery path, and return both the collected result and that error. -Handle zero-entry failures and cancellation consistently with R04. Add a narrow -fault-injected regression for entries-plus-error, verifying readable attempts, -retained causes, and API/CLI partial-result behavior. This concerns data -preservation, separate from R04's cancellation/error classification. - -## Follow-up stage review evidence - -The subsequent review examined all eight completed stages in the implementation -plan, including the R01–R13 corrections, at `363f7cd`. Existing tests for -classification, Git endpoints, records, public recovery, publication evidence, -discovery, CLI parsing/presentation, cold Go-tool handoffs, and the assembled -lost-output workflow passed with Go 1.26.8. The compiled facade/dependency checks -and local documentation link/fragment checks also passed. - -Disposable local probes reproduced R14–R17 using temporary Git repositories and -Go source overlays. Repository production code and tests remained unchanged. -No additional defects were identified in Stages 3–6. Stage 8 needs regression -coverage for the cases below and plan wording aligned with the implemented -record and handoff decisions. Existing passing coverage does not establish -correctness for these additional combinations. - -## R14: Support recovery for vendored API-only consumers — P2 - -**Completed:** API-only recovery uses a private module root with preserved module -settings and relocated local replacements. Vendored API-only, relative/versioned -replacement, cold Go-tool, native handoff, and source-module entry-point tests -passed. The assembled vendored case also runs publication through a private -Go-tool launcher, removes its metadata, and executes the later handoff from -another directory. Stage 7/8 instructions and consumer/internal guidance are -updated. - -**Relevant code:** `internal/gotool/recovery.go:95–107`; the command assembly in -`cmd/releasekit/main.go:126–145`. Affects Stage 7 and its Stage 8 coverage. - -The API-only handoff prepares its private alternate module files with -`go list -mod=mod`, then prints a `go tool` command running from the consumer's -module directory. Go automatically selects vendor mode when that directory -contains a suitable `vendor` tree. An API-only consumer vendors its imported -library packages, so the CLI package can be absent from that tree. - -**Evidence:** A temporary consumer imported ReleaseKit through its Go API, -vendored its dependencies, and used the current CLI for a locally rejected -publication. A complete record and recovery command were produced. Executing -the command failed before reconciliation with `import lookup disabled by --mod=vendor`. Supplying `GOFLAGS=-mod=mod` also failed to change this Go-tool -behavior. Existing vendored-tool coverage uses an explicitly registered and -vendored CLI, which does not exercise this API-only case. - -**Expected behavior:** A consumer's ordinary library vendoring remains compatible -with the supported API-only recovery handoff. The command observes the explicitly -selected record using the selected ReleaseKit dependency. - -**Remediation:** Prepare the API-only Go-tool entry point in a private bootstrap -module root whose tool resolution is independent of the consumer's vendor tree. -Preserve the selected dependency graph, checksums, versioned replacements, and -the meaning of relative local replacements when relocating module metadata. -Keep source files and consumer vendoring intact, retain private storage handling, -and keep bootstrap ownership within ReleaseKit. Preserve registered-tool and -native entry points. Update Stage 7's handoff instructions to describe this -module-selected approach and the supported native fallback. - -**Regression coverage:** Add an assembled API-only consumer that vendors the -library without a tool declaration. Execute the printed command from another -directory after publication and launcher cleanup; assert that reconciliation -runs against the saved identity and returns its expected state/status. Cover -relative local replacements and unchanged consumer source/vendor contents. -Retain existing cold-build, registered/vendored-tool, quoting, and status cases. - -## R15: Preserve the recovery handoff after publication cancellation — P2 - -**Completed:** Post-failure handoff preparation has a separate bounded context -and retains a persistent native fallback after module-preparation failure. -Coordinated native, cold registered-tool, and temporary-launcher cancellation -tests passed, including execution of the printed command after cleanup and -retained cancellation diagnostics/statuses. Temporary Go-work executable -classification is covered, and Stage 7/8 plus canonical guidance is updated. - -**Relevant code:** `cmd/releasekit/main.go:116–134`, especially the caller context -passed to `git.CommonDirectory`. Affects Stage 7 and its Stage 8 coverage. - -Command preparation reuses the publication context after publication returns. -When cancellation caused the failure, that context is already canceled. The -metadata lookup fails immediately and the CLI prints `recovery command -unavailable`, even when it has saved the record and a usable native entry point -is available. The native fallback is reached only when module selection returns -no Go invocation; a preparation error bypasses it. - -**Evidence:** A probe synchronized cancellation with a blocked tag push. The -publication returned status 1 after immediate reconciliation found the tag -absent. Standard error reported a saved record and an unavailable recovery -command, with no copyable handoff. This was cancellation during execution, -rather than termination before the record could be saved. - -**Expected behavior:** An ordinary canceled publication with a saved record -retains a usable recovery entry point. Its preparation leaves the original -publication outcome, cause, and exit classification intact. - -**Remediation:** Arrange handoff preparation so the failed publication's canceled -context does not prevent command construction. Prepare necessary durable launch -metadata before cancellation-sensitive publication work where appropriate, and -retain a usable native-command fallback when module-based preparation fails. -Account for cold Go-tool executables that disappear when the original driver -exits. Keep publication and later observation cancellation behavior from R04; -any separate post-failure preparation that performs subprocess work needs a -bounded lifetime. Update Stage 7 to cover cancellation and preparation failures. - -**Regression coverage:** Cancel an actively blocked push after pre-push recording, -then execute the resulting command using a fresh invocation/context. Cover a -persistent native executable and supported cold Go-tool adoption paths after -launcher cleanup. Assert saved identity selection, unchanged publication status -and original cancellation cause, and preservation of immediate recovery's bounded -context. Coordinate the push boundary explicitly and retain the existing -no-recovery-step behavior for successful publication. - -## R16: Classify endpoint setup failures by their actual cause — P3 - -**Completed:** The Git boundary marks configuration-read/resolution failures; -observation maps those, positive rewrites, and other operational errors to the -existing kinds while preserving causes. Named/unnamed malformed configuration, -rewrite, tooling, cancellation, facade, and CLI diagnostic tests passed. Tests -also verify no tag query after setup failure; Stage 1/6/8 guidance is updated. - -**Relevant code:** `internal/publish/reconcile.go:116–128` and -`internal/git/remote.go`'s endpoint setup. Affects Stages 1 and 6. - -An `ObserveEndpoint` error defaults to `endpoint_changed` when the evidence has -no original remote name. With a name, the fallback is `transport_configuration`. -The choice can therefore describe the presence of optional context rather than -the failure that occurred. - -**Evidence:** A local checkout with malformed Git configuration and evidence -whose `Remote` was empty returned `unknown` with `endpoint_changed`. Its retained -cause was Git's bad-configuration error; no changed destination had been -established. CLI guidance consequently points the operator toward URL rewrite -settings despite the configuration-read failure. - -**Expected behavior:** `endpoint_changed` identifies a positively detected -destination change. Configuration and other operational setup failures retain -an appropriate unresolved classification and their original causes, including -when the original remote name is absent. - -**Remediation:** Distinguish setup failures at the internal Git boundary where -their source is known, then map them to the supported recovery kinds. Use the -existing endpoint-change sentinel for positively detected rewrites. Preserve -configuration/tooling causes through the facade and use the same interpretation -for human guidance and structured diagnostics. Clarify this mapping in Stage 1 -and its Stage 6 presentation requirements using the existing recovery kinds. - -**Regression coverage:** Exercise malformed local configuration with an empty -original remote name, a genuine rewrite mismatch, and a separate operational -setup failure at the Git boundary. Verify typed kinds, retained causes, unknown -state, and corresponding CLI human/JSON diagnostics. Keep remote queries from -reaching a substituted endpoint when a rewrite mismatch is detected. - -## R17: Validate the documented version-2 byte-field representation — P3 - -**Completed:** Known byte fields load through explicit JSON string validation and -standard base64 decoding. Tests passed for valid/empty strings, numeric arrays, -other structural types, malformed base64, and retained decoding causes, alongside -existing partial/unknown-field, version-1, and lossless endpoint regressions. -Stage 2/8 instructions and the canonical format guide are updated. - -**Relevant code:** `internal/publish/record.go:48–59`; -[private record format](../integrations/publication-records.md#private-attempt-record). -Affects Stage 2 and its Stage 8 format coverage. - -Version-2 byte fields are documented as standard base64 strings. The loader -unmarshals each known field directly into `[]byte`; Go's JSON decoder also accepts -numeric arrays for that destination type. Consequently, an invalid structural -type under the documented format is silently treated as usable evidence. - -**Evidence:** Both `"djEuMi4z"` and `[118,49,46,50,46,51]` as the value of -`evidence_bytes.tag` loaded as `v1.2.3` without a format error. The array case is -outside the declared representation. This is a format-validation gap; the probe -did not demonstrate altered bytes or an incorrect remote observation. - -**Expected behavior:** Known byte fields follow the documented base64-string -representation. Structural and encoding failures return `record_format` with -their available causes. Supported partial evidence and ignored additional fields -retain their existing behavior. - -**Remediation:** Decode known byte-field values as strings explicitly, then use -the standard base64 decoder to recover their bytes, preserving decoding causes. -Keep version-1 behavior and version-2 lossless identity from R10. Update Stage 2 -to describe both supported versions and the writer's version selection, linking -to the canonical format definition. - -**Regression coverage:** Add known byte-field cases for valid base64, malformed -base64, numeric arrays, and other invalid structural types. Assert typed format -errors and retained causes for invalid inputs. Reuse the existing all-field -round trips, non-UTF-8 endpoint observation, partial-evidence, and unknown-field -tests rather than duplicating their fixtures. - -## Implementation plan and verification follow-through - -**Completed:** [implementation.md](implementation.md) records the R14–R17 -corrections alongside their implementation: - -- Stage 1 records the cause-based endpoint setup classification from R16. -- Stage 2 describes ordinary version-1 records and version-2 byte-preserving - records, including the structural validation from R17. -- Stage 6 aligns typed failures, human guidance, and structured diagnostics with - R16 and retains the implemented JSON output-failure handling from R06. -- Stage 7 replaces the original executable-only handoff instructions with R07's - module-selected/durable API-only approach, extended for vendoring and - cancellation by R14 and R15, including native fallback and explicit record - selection. -- Stage 8 includes the regression coverage assigned above and updated canonical - documentation. Final assembly passed `go test ./...`, - `go run ./internal/apicheck`, and `git diff --check`; local links and fragment - destinations were checked. Validation used Go 1.26.8 with offline dependencies. - -Earlier completion evidence and the distinction between original and follow-up -corrections are retained. Each issue has a completion marker. The corrections -retain the existing public facade, publication completion point, separate -recording failures, and operator-owned recovery choices; they introduce no -consumer pilot, intermediate release, or external review gate.