Prepare v0.8.0 notes and retire completed roadmaps

This commit is contained in:
2026-11-03 00:29:51 +00:00
parent ee72566263
commit 37d6209aec
5 changed files with 53 additions and 1826 deletions

View File

@@ -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-<random>.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).

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.