Skip to content

feat: stamp the version into the release payload and smoke-test it#130

Open
REPPL wants to merge 1 commit into
feat/itd-67-p3-launch-ship-changelogfrom
feat/itd-67-p4-manifest-install-smoke
Open

feat: stamp the version into the release payload and smoke-test it#130
REPPL wants to merge 1 commit into
feat/itd-67-p3-launch-ship-changelogfrom
feat/itd-67-p4-manifest-install-smoke

Conversation

@REPPL

@REPPL REPPL commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Phase 4 — the final phase of the derived-versioning + auto-changelog + plugin-distribution
programme (plan: .abcd/development/plans/2026-07-21-derived-version-and-changelog.md;
spec: spc-11).

Fifth of five stacked PRs. Based on feat/itd-67-p3-launch-ship-changelog (#129).

The version lands on the payload, never on the tree

RenderPayload stamps the derived version into the payload's copies of plugin.json and
marketplace.json — primary, marketplace secondary, and the changelog entry — in lockstep.

ADR-19 holds: git diff … -- .claude-plugin/ is empty, both committed manifests carry
zero version keys, and a test asserts a render leaves the working tree's manifests
byte-identical. The new .abcd/config/version-location.json is a decision artefact: it
declares manifest_path and an RFC-6901 json_pointerwhere a version goes, never what
it is. The seed figure ADR-19's prose mentions was deliberately omitted, since outcome 4
says the base is the newest git tag and a seed would be both stale and the one value in the
file readable as a version.

The lockstep gate was inert, and is now live

version-location.json was absent, so CheckLockstep returned unreadable and
launch --dry-run could never pass. Adding it exposed a second fault, which the review
caught: DryRun and Ship asserted the public polarity against the working tree
which under ADR-19 is version-absent forever. The gate could never be satisfied, and its
diagnostic told the operator to add the very key the policy forbids.

Both now assert TreeDev over the source tree; TreePublic stays where it belongs, over the
rendered payload.

before:  lockstep {"tree":"public","ok":false,"unreadable":true,…}   would_refuse_on: 2 reasons
during:  lockstep {"tree":"public","ok":false,"drifts":[…absent-key…]} would_refuse_on: 3 reasons
after:   lockstep {"tree":"dev","ok":true,"exit_code":0}              would_refuse_on: none

version now reads 0.3.0, sourced from the CHANGELOG's dated heading rather than from a
manifest read. resolveVersion is deleted — post-ADR-19 there is no manifest version to read.

The light smoke

Manifests parse, source resolves, and every declared command/agent/hook path exists in the
resolved payload bundle
— not in the working tree, because a file present in the tree but
excluded from the payload is exactly the bug this catches. Wired into DryRun and Ship, so
it gates a release rather than merely testing one.

The phase-4 STOP was not hit, and it is addressed structurally rather than asserted: a
PayloadTree interface splits resolution from assertion, and
TestPayloadTreeImplementationsResolveIdentically pins both implementations to the same
resolved list. itd-66's deferred deep tier reuses the resolution unchanged.

It also surfaces iss-110 honestly rather than hiding it: the resolved surface lists
agents/CHANGELOG.md and agents/README.md as kind: agent, because filtering them here
would conceal the defect that issue tracks.

What the review caught — three FIX_FIRST, all fixed

  1. A refused payload render left a written release record behind. The render — including
    the smoke, the gate meant to stop a broken release — ran after release.Ingest had
    written the dated heading. Reproduced twice: exit 2 with a message that never mentions the
    write, CHANGELOG.md carrying ## [0.4.1], files staged in the destination, and the retry
    then permanently refused as release-in-flight. The version-free half of the render is now
    hoisted into PrecheckPayload and runs before the write, with a rollback backstop that
    restores the record and says so if a later error slips through.
  2. The dry-run accused this repository's correct manifests of drift (the polarity fault
    above).
  3. The README claimed a version comparison that cannot happen. source: "./" means the
    harness reads the committed manifest, which by ADR-19 carries no version, and release.yml
    publishes only binaries and checksums. The claim is replaced with what is true today.

Verification

gofmt -l . silent · go vet ./... · go build ./... · go test ./... · go test -race ·
go run ./cmd/record-lint exit 0 · go run ./cmd/abcd docs lint 0 blockers ·
go run ./cmd/abcd launch --dry-run clean.

Tested under CI's actual condition: full suite green in a clone with its tags deleted.

CHANGELOG.md, go.mod, go.sum, .claude-plugin/** and every .github/workflows/*.yml
are untouched.

Noted, not fixed

launch.Ship() / ShipRequest has no caller outside its own tests — the CLI's
abcd launch ship goes through release.Emit/release.Ingest. Confirmed identical on #129,
so this is pre-existing itd-65 scaffolding, left alone per "leave pre-existing dead code
alone — mention it".

Phase 4 of the derived-versioning programme (itd-67 / spc-11), and the last.

THE VERSION LANDS ON THE PAYLOAD, NEVER ON THE TREE
`RenderPayload` stamps the derived version into the payload's copies of
plugin.json and marketplace.json — primary, marketplace secondary, and the
changelog entry — in lockstep. The working tree's manifests stay version-absent
(ADR-19) and a test asserts a render leaves them byte-identical. The new
.abcd/config/version-location.json is a decision artefact: it says WHERE a
version goes (an RFC-6901 path), never what it is.

THE LOCKSTEP GATE WAS INERT AND IS NOW LIVE
version-location.json was absent, so CheckLockstep returned unreadable and
`launch --dry-run` could never pass. Adding it exposed a second fault: DryRun and
Ship asserted the PUBLIC polarity against the working tree, which under ADR-19 is
version-absent forever, so the gate could never be satisfied and its diagnostic
told the operator to add the very key the policy forbids. Both now assert TreeDev
over the source tree; TreePublic stays where it belongs, over the rendered
payload. The dry-run reports lockstep dev/ok and no refusals for the first time.

THE LIGHT SMOKE
Manifests parse, `source` resolves, and every declared command/agent/hook path
exists in the resolved payload bundle — not in the working tree, because a file
present in the tree but excluded from the payload is exactly the bug this
catches. Wired into DryRun and Ship, so it gates rather than merely tests.
Resolution is split from assertion behind a PayloadTree interface, with both
implementations pinned to the same resolved list, so itd-66's deferred deep tier
reuses the resolution unchanged.

NOTHING PARTIAL, EVEN ACROSS STEPS
The version-free half of the render — destination checks, bundle resolution and
the smoke — is hoisted into PrecheckPayload and runs BEFORE the changelog write,
so a payload refusal can no longer leave a written release record behind a
staged directory. A rollback backstop restores the record if a later error slips
through, and the message says which happened.

Assisted-by: Claude:claude-opus-4-8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant