Skip to content

Real version-bump wrap-step handler (open-queue #3 post-#139)#202

Merged
Jason-Vaughan merged 2 commits into
mainfrom
feat/version-bump-handler
May 22, 2026
Merged

Real version-bump wrap-step handler (open-queue #3 post-#139)#202
Jason-Vaughan merged 2 commits into
mainfrom
feat/version-bump-handler

Conversation

@Jason-Vaughan
Copy link
Copy Markdown
Owner

What

Replaces the last remaining no-op stub in lib/wrap-steps/version-bump, sitting unchanged since #139 Chunk 3 — with a real handler that promotes CHANGELOG.md's [Unreleased] section to a dated release and bumps version.json.

Why

version-bump was the final piece of the V2 wrap pipeline still returning a placeholder {ok:true, status:'done'} from the early Chunk-3 scaffolding. After PR #200 populated the prawduct ai-content prompts, this is the cleanest remaining open-queue item before V2 prawduct wraps produce complete output end-to-end. Closes that gap and unblocks future v3.17.0 release cuts being driven through the V2 pipeline rather than manual version.json + CHANGELOG edits.

How

Bump-level precedence

  1. options.bumpLevel override (patch | minor | major) — wins outright if in the allowed set
  2. BREAKING marker anywhere in [Unreleased] body → major
  3. ### Added / ### Changed / ### Removed / ### Deprecated present → minor
  4. Otherwise (only ### Fixed / ### Security) → patch

Single-transaction discipline + composite-staged-keys

Handler never writes the filesystem; it stages two entries under composite keys:

context.staged['version-bump:version-json'] = {primingPath, newContent, changed:true, oldVersion, newVersion, bumpLevel}
context.staged['version-bump:changelog']    = {primingPath, newContent, changed:true, oldVersion, newVersion, bumpLevel}

The Chunk-9 commit step's _flushStagedWrites is already duck-typed on {primingPath, newContent, changed} — it iterates Object.entries(staged) and flushes any matching shape, so both composite-keyed entries get written without a dispatch-table edit. This establishes the multi-file write-staging convention for any future step that needs to stage more than one file write.

Commit body line

lib/wrap-steps/commit.js:_buildBodyLines gains a new duck-type matching {oldVersion, newVersion, bumpLevel} — emits - Bumped <old> → <new> (<level>) once per pipeline run (deduped via a local emittedVersionBump flag since both staged entries carry the metadata). Pinned with a dedupe test (both insertion orders).

Never blocks

Per the ADR 0002 step-kind contract ("Optional, never blocks"). Every degraded condition — missing version.json, malformed JSON, non-semver version field, missing CHANGELOG.md, missing [Unreleased] section, empty [Unreleased] body — returns {ok:true, status:'skipped'} with output.reason + output.detail for the wrap drawer to render inline.

Idempotent on re-wrap

After a successful bump, the [Unreleased] body is empty (just the heading + blank line). On the next wrap the handler observes no entries and returns {status:'skipped', output:{reason:'no entries to promote'}} — no double-bump. Pinned with a dedicated test.

Banner emoji NOT auto-injected

> 🛟 (bug-fix) / > 🚀 (feature) leading banners stay a curated operator decision per the project's CHANGELOG convention. The handler promotes [Unreleased] body byte-for-byte under the new dated heading; banner insertion is out of scope and tracked separately in the build plan.

Output for the wrap drawer

output.from, output.to, output.bumpLevel, output.detail (pre-formatted "3.16.2 → 3.17.0 (minor)" string). Matches the existing public/wrap-drawer.js:deriveDetail's version-bump case (if (output.from && output.to) return …) — no drawer-side change required.

Test plan

  • 39 net new tests in test/wrap-pipeline.test.js:

    • _parseSemver (5): canonical x.y.z, all invalid forms (no v prefix, no pre-release, partial, non-string).
    • _bumpSemver (5): patch / minor / major arithmetic + reset behavior + invalid input.
    • _parseUnreleased (6): finds + categorizes, stops at next ## [ heading, hasEntries true/false, ok:false on missing, nullish-safe, dedupes duplicate subsections.
    • _decideBumpLevel (6): override wins, unknown-override fallthrough, BREAKING marker, minor-trigger subsections, patch-only path, mixed-subsection vote, default to patch.
    • _promoteUnreleased (4): promote shape pin, no auto-banner injection, no-op when [Unreleased] missing, multi-paragraph + nested-bullet preservation.
    • Handler (13): skip when no project.path, skip when version.json missing, skip when version.json malformed, skip when non-semver, skip when CHANGELOG.md missing, skip when [Unreleased] missing, skip when [Unreleased] empty, happy path stages both writes + populates output, options override wins, BREAKING forces major, idempotent on re-wrap, only Fixed → patch, never-blocks contract sweep.
    • Commit body-line (2): renders Bumped line from staged metadata, deduped across the two staged write entries (both insertion orders).
    • Stub-block sentinel (1): kinds array now empty + sentinel pin so a future stub-introducing chunk has to update the list.
  • Full suite: 2324 / 2324 pass on this branch (was 2285 on main post-PR-Populate prawduct ai-content prompts (open-queue #2 post-#139) #200; net +39).

Migration note

This is template-and-handler-only; no schema changes, no template structure changes. Existing prawduct projects pick up the new handler automatically on next TC server restart (no methodology template cache invalidation needed since the step's kind and id are unchanged — only the implementation behind the kind moved from no-op to real). Projects that don't have a CHANGELOG.md or version.json continue to see the step report skipped cleanly; no breaking behavior.

Files touched

  • lib/wrap-steps/version-bump.js — replaces no-op stub with real handler
  • lib/wrap-steps/commit.js — adds version-bump duck-type to _buildBodyLines
  • test/wrap-pipeline.test.js — adds 41 new tests + adjusts 2 existing tests for the stub-→-real transition (Chunk-3 stub-block sentinel + monkey-patch list in the "no-op stubs end-to-end" test)
  • docs/adr/0002-wrap-pipeline-contract.md — Status line extension marker
  • CHANGELOG.md[Unreleased] ### Added entry

Out of scope

  • Auto-injection of > 🛟 / > 🚀 release banners (curated decision per repo convention).
  • GitHub Release creation (gh release create) — separate step per CLAUDE.md "Releases & Versioning".
  • Pre-1.0 version handling refinement (TC is at 3.x; documented in handler module docstring).
  • End-to-end smoke test on a real wrap session — Chunk 2 of the build plan, post-merge.
  • Per-field reconciler for step.prompt (cross-cutting concern shared with PR Populate prawduct ai-content prompts (open-queue #2 post-#139) #200's migration note).

The full build plan lives at .claude/plans/version-bump-handler.md (project-local, gitignored per CLAUDE.md plan convention).

Replaces the last remaining no-op stub in `lib/wrap-steps/`
(`version-bump`, sitting unchanged since #139 Chunk 3) with a real
handler that promotes `CHANGELOG.md`'s `[Unreleased]` section to a
dated release and bumps `version.json`.

**Bump-level precedence:**

1. `options.bumpLevel` override (`patch` | `minor` | `major`)
2. `BREAKING` marker anywhere in [Unreleased] body → `major`
3. `### Added` / `### Changed` / `### Removed` / `### Deprecated`
   present → `minor`
4. Otherwise (only `### Fixed` / `### Security`) → `patch`

**Single-transaction discipline + composite-staged-keys.** Handler
stages two entries under composite keys (`'version-bump:version-json'`
and `'version-bump:changelog'`) so the Chunk-9 `commit` step's
`_flushStagedWrites` flushes both. Establishes the multi-file
write-staging convention for any future step that needs more than one
file write.

**Commit body line.** `lib/wrap-steps/commit.js:_buildBodyLines` gains
a new duck-type matching `{oldVersion, newVersion, bumpLevel}` — emits
`- Bumped <old> → <new> (<level>)` once per pipeline run, deduped via
a local flag since both staged entries carry the metadata.

**Never blocks** (matches the ADR 0002 step-kind contract). Every
degraded condition — missing `version.json`, malformed JSON, non-semver
version field, missing `CHANGELOG.md`, missing `[Unreleased]` section,
empty `[Unreleased]` body — returns `{ok:true, status:'skipped'}` with
a `reason` / `detail` for the wrap drawer to render inline.

**Idempotent on re-wrap.** After a successful bump the `[Unreleased]`
body is empty; next wrap observes no entries and skips.

**Banner emoji NOT auto-injected.** `> 🛟` (bug-fix) / `> 🚀` (feature)
banners stay a curated operator decision; the handler promotes
`[Unreleased]` body byte-for-byte under the new dated heading.

**Test coverage.** 39 net new tests in `test/wrap-pipeline.test.js`:
- Pure-helper decision tables for `_parseSemver`, `_bumpSemver`,
  `_parseUnreleased`, `_decideBumpLevel`, `_promoteUnreleased`.
- Handler tests for every skip path + happy path + idempotency +
  override + BREAKING-marker + never-blocks contract sweep.
- Commit-body-line duck-type test + dedupe pin.

The Chunk-3 `wrap-pipeline step stubs` describe block updates: `kinds`
array marked empty with a sentinel pin (`assert.equal(kinds.length, 0,
…)`) so any future stub-introducing chunk has to add its kind and
trip the sentinel — explicit handoff signal rather than vacuous block.

ADR 0002 Status line extended with the open-queue-#3 marker.

Full suite: 2324 / 2324 pass.
@Jason-Vaughan Jason-Vaughan added the enhancement New feature or request label May 22, 2026
m1 (MINOR): wrap-drawer.js carried a stale "Chunk-3 no-op stub"
comment for the `version-bump` case in `deriveDetail`. Now that the
real handler ships in this same PR, the comment was wrong as of
merge. Replaced with a one-liner summarizing the actual emitted
shape: `{from, to, bumpLevel, detail}` on done, `{skipped, reason,
detail}` on skip.

n1 (NIT): `BREAKING_RE` was `\bBREAKING\b` — broader than the build
plan documented (`BREAKING:` or `(BREAKING)` markers). Tightened to
`/\bBREAKING(?::|\s*\()/` so casual uppercase `BREAKING` in prose
(e.g. `## NOT BREAKING — just renamed`) no longer falsely forces a
major bump. Added a new `_decideBumpLevel` test covering the
false-positive shapes the old regex would have matched; updated the
existing test data to include the colon so the assertions stay
meaningful with the tighter regex.

n4 (NIT): documented in the module docstring that `version.json` is
always re-serialized to 2-space indent + trailing newline; projects
using a different style will have the bump re-normalize silently.
Open follow-up if a methodology adopts a different style.

Coverage gaps (3 new pins):

- Byte-exact whitespace pin: `_promoteUnreleased` emits exactly one
  blank line between `[Unreleased]` and the new dated heading. A
  stray double-blank would not previously have been caught.

- Cross-file invariant pin: promoted CHANGELOG output goes through
  the same regex detectors as `test/changelog-structure.test.js`
  (descending semver order + no-dupes + heading-parseable). Drift
  surfaces as a test failure in BOTH places — the desired symmetry.

- Stage-+-flush integration test: new describe block bridging
  `versionBump.run` and `commitStep._flushStagedWrites`. Proves the
  two composite-keyed staged entries both make it to disk on a
  single flush call, with bytes on disk matching the staged content.

The 4th gap (drawer-side `deriveDetail` for version-bump skip path)
is already covered by `test/wrap-drawer.test.js:196-205`.

n2 (NIT — CHANGELOG descending-order runtime guard) and n3 (NIT —
`output.skipped:true` convention lockstep across handlers) filed as
follow-up issues per Critic's recommendation; both touch
out-of-scope surface beyond this chunk's mandate.

Full suite: 2328 / 2328 pass (+4 from this fixup commit; PR total
now +43 vs main).
@Jason-Vaughan Jason-Vaughan merged commit fe27a48 into main May 22, 2026
@Jason-Vaughan Jason-Vaughan deleted the feat/version-bump-handler branch May 22, 2026 23:18
Jason-Vaughan added a commit that referenced this pull request May 23, 2026
Eats its own dogfood: invokes the new `lib/wrap-steps/version-bump.js`
handler (shipped in PR #202 a few hours ago) programmatically against
this repo, then flushes the two staged writes via the same
`commit._flushStagedWrites` the V2 wrap pipeline would use.

Handler output:

  - oldVersion: 3.16.2
  - newVersion: 3.17.0
  - bumpLevel: minor (subsections: Changed, Fixed, Added)
  - detail: 3.16.2 → 3.17.0 (minor)

Highlights of this release (full notes in CHANGELOG.md):

- **#139 Methodology-aware single-button session wrap (series closed).**
  12 chunks across the May 14–19 window. `wrapV2: true` is now the
  default. Full server-side pipeline (`pr-check` → `critic-check` →
  `version-bump` → `ai-content` × 3 → `priming-roll` → `commit`)
  replaces the legacy NL-prompt-via-tmux flow. ADR 0002 is the
  durable home.

- **Open-queue #2 (PR #200): prawduct ai-content prompts populated.**
  Three placeholder steps (`changelog-update` / `learnings-capture` /
  `memory-update`) gain real prompts. V2 wraps now produce CHANGELOG
  entries / `learnings.md` entries / MEMORY.md session blocks
  instead of three SKIPPED rows.

- **Open-queue #3 (PR #202): real version-bump handler.** The handler
  that cut THIS release. Last #139-era no-op stub replaced.

- **PR #191: tmux env-ordering hotfix.** Engine `launch.env` now
  reaches the spawned engine process (was silently dropped pre-fix).
  Surfaced via Aider / LiteLLM `OPENAI_API_KEY` integration.

- **#168 CHANGELOG structural-invariants test.** Pinned post-PR #166
  regression class.

CHANGELOG `[Unreleased]` heading retained at the top with an empty
body (per the handler's promote contract) so the next session has
somewhere to accumulate entries.

Banner emoji NOT auto-injected — to be added manually as `> 🚀` (this
is a feature release, not a bug-fix) in a follow-on commit or
release-notes edit per the project's banner convention.

The UTC-vs-local-date bug in the version-bump handler (surfaced
cutting this release: handler stamped 2026-05-23 in UTC; manually
patched to 2026-05-22 to match the local-zoned convention used by
every prior CHANGELOG entry) is filed as #205. Future releases pick
up the fix when that lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant