Development record
derived-versioning
Summary
spc-10 delivers itd-73: the version is a fact derived from what shipped, never a
number a human types. It adds one product judgement — impact — to the records
that already exist, makes that judgement mandatory at the lifecycle gates that
matter, derives the next SemVer from the set of records cut since the last git
tag, and guards the derived number against a mislabel with a structural surface
diff.
The programme plan is
.abcd/development/plans/2026-07-21-derived-version-and-changelog.md; its
Outcomes section is authoritative where it touches anything below.
Scope
- The
impactfield — new frontmatter on intents and issues, enumadditive | breaking | fix | internal, set explicitly; there is no silent default.internalmeans "excluded from the changelog and drives no bump". Intents are neverinternal— a press-release-first intent is user-facing by definition. - Two blocker lints (
internal/core/lint, reusing the existing intent-folder and issue-ledger scanners rather than adding a walk):intent_impact_valid— an intent may not sit inshipped/without a valid, non-internalimpact.issue_impact_valid— an issue may not sit inresolved/without a validimpact.
- A once-only back-fill of the records that predate the field: the historical
shipped intents and the issues resolved since
v0.3.0. Agent-proposed, maintainer-confirmed; a bounded set, not a rolling debt. - Version derivation (
internal/core/changelog, arithmetic reusinginternal/core/launch/semver.go):deriveNext(prev, bump)over the pre-1.0 table — while at0.x,breakingbumps the minor, everything else the patch; at>= 1.0the standard SemVer table applies. The first1.0.0is an explicit override, never derived. - The tag-anchored release set — the base version is the newest git tag,
and the set of records in the cut is the set-difference of
git ls-tree <tag> -- <shipped/, resolved/>againstgit ls-tree HEADover the same paths, minus every record whoseimpactisinternal. If the newest CHANGELOG heading is ahead of the newest tag, a release is in flight and derivation refuses rather than deriving against a mismatched base. - The surface-diff guardrail — a deterministic structured snapshot of the
command/flag/manifest surface, committed and drift-tested, diffed at the cut. A
removed or renamed command or flag, a newly-required flag, or a removed
declared manifest surface entry, with no
breakingintent in the cut, fails the launch and names the surface. Additions, help-text changes, and reordering are not breaks.
Approach
Every input already exists as a record; the only new datum is the one-word
product judgement. Directory-as-truth stays the lifecycle authority — impact
is a property of a record, never its status — so the lints gate the transition
into the terminal folder rather than inventing a state machine.
Derivation is anchored on the git tag because it is the only immutable anchor:
the CHANGELOG heading and the tag describe the same release only outside the
post-merge/pre-tag window that auto-release.yml opens, so reading the base from
the heading would derive against a phantom base. The set is computed as a
tree diff of end-states, not a log walk of moves, which makes it immune to
squash and rebase merges — the itd-73 STOP condition. Its known limits are pinned
by tests, not hidden: a within-shipped/ slug rename reads as delete+add, and an
intent moved out of shipped/ (supersession) surfaces as a Removed line
rather than silently vanishing.
The guardrail's snapshot is a new structured JSON emitter over the shared
NewRootCommand() tree — the one-canonical-primitive is the root command, not
the Markdown walker GenerateReference uses. That walker emits prose, excludes
the hidden hook subtree, and carries neither per-flag required-ness nor the
manifest surface, so reusing it would encode blind spots into the gate. The
baseline is seeded at the current release commit and gated by a drift test in the
same shape reference_test.go gates commands.md; a cut with no committed
baseline is fail-closed, because the first cut is the highest-risk one.
impact drives the version only. It never decides a Keep-a-Changelog section: a
four-value enum cannot express Security/Deprecated/Removed granularity, and
conflating the two would make the version hostage to editorial judgement.
Acceptance-criteria satisfaction
additive→ minor,breaking→ major,fix→ patch (AC 1–3) —deriveNextis a pure function with a table-driven test over every cell, including the pre-1.0 row (0.x+breaking→ minor) and the empty set (no bump, "nothing to release").- A surface break with no
breakingintent FAILS and names the surface (AC 4) — the guardrail tests assert both directions: a removed command with nobreakingintent fails and names it; the same removal with abreakingintent in the cut passes. A newly-required flag is tested as a break; a new optional flag and a reordering are tested as non-breaks. - Every intent carries a valid
impactbefore it can ship (AC 5) —intent_impact_validis a blocker; its test watches an intent with an absent, misspelled, andinternalimpacteach fail the move toshipped/, and a valid one pass.issue_impact_validmirrors it forresolved/. - The working tree stays unversioned; the number lands on the release artefact
(AC 6) — the dev-tree
plugin.jsonstays version-absent (ADR-19); the derived number is rendered into the release payload only, and its single in-tree carrier is the CHANGELOG dated heading (ADR-37). Covered by the lockstep release-mode tests.
What this spec does not deliver
- The changelog prose and the
launch shipwrite-verb that consumes the derived number — spc-11 (itd-67). - Pre-release channels (alpha/beta/rc), per-consumer compatibility ranges, and deprecation windows — out of scope on itd-73's own terms.
- Behavioural breaks behind an unchanged surface — the author's
breakingjudgement, which no structural diff can see. Documented as a limit, not hidden.
Frontmatter
| id | spc-10 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-10-derived-versioning.md
open on GitHub ↗ · commit history ↗
References1
- itd-73abcd Cuts the Version — You Never Type Oneimplements
mentions10
- adr-19The plugin version lives ONLY in the released artifact; the working tree stays unversioned, and the version location is chosen by a schema-validated decision, not hard-coded
- adr-37Releases are changelog-driven — rolling `[Unreleased]` is the release decision, and automation tags exactly that commit
- dcn-26072100000000022026-07-21 — itd-67's "a phase completed since the last launch -> minor" bump
- dcn-26072100000000042026-07-21 — `impact` is a KNOWN property of the issue ledger schema, and
- iss-115There is no uniqueness lint for spec ids, so two concurrent branches always mint the same spc-N. abcd mints the next free id from the local tree, so any two branches cut from the same base collide silently: the itd-73/itd-67 programme and the itd-95/itd-96 programme both minted spc-10 and spc-11 on 2026-07-21. The collision is invisible on each branch and only appears on merge, where it breaks the bidirectional intent-spec link in BOTH directions at once — 'itd-95 names spc-10, but spc-10 claims itd-73' and 'itd-67 names spc-11, but spc-11 claims itd-96' — because whichever spec wins the store lookup leaves the other intent's link dangling. iss-74 added an issue-id uniqueness lint; specs need the same guard, and record-lint is the natural home so CI catches it on the PR rather than a human catching it at merge. Consider also whether minting should reserve ids in a way concurrent branches cannot duplicate.
- iss-120Sequential record ids are minted from the local tree, so two parallel programmes mint the same next id — both took spc-10, spc-11, iss-110 and iss-111, costing four renumber commits across the episode. This generalises iss-115 (spec ids lack a uniqueness lint): issue and intent ids have armed detectors via the shared validateIDUnique primitive, spec and ADR ids have none, and detection-after-collision is the only mitigation anywhere. The design call is whether detection everywhere is enough or minting itself should be made collision-free (id-range leases per programme, mint-at-merge), which trades away human-readable sequential ids.
- iss-2609230733586740itd-73 ac-5 promises that an intent captured or shaped with an absent or invalid impact is flagged as a blocker; delivered reality: intent_impact_valid flags an invalid or internal value in every bucket but an ABSENT impact only in shipped/ (internal/core/lint/lint.go checkIntentImpact), and CreateFromText leaves impact optional on a draft — so a draft or planned intent carries no impact until the spec close forces one. Narrower than the criterion; spc-10 states the shipped/ bar only. Found by the itd-73 fidelity audit (rcp-0d18ea2d4682).
- itd-67abcd Is An Installable, Versioned Claude Code Plugin Whose Repo Is Its Own Marketplace And Whose Every Launch Bumps, Tags, And Publishes A New Version
- one-writer-per-fileOne writer per file
- spc-11installable-versioned-plugin