Development record
abcd Cuts the Version — You Never Type One
Press Release
Ship intents; abcd derives the version. You work in features and intents, never in
vX.Y.Z. Each intent declares one thing about itself — whether it adds, breaks, or fixes — and when/abcd:launchcuts a release, abcd reads the intents that shipped since the last release and computes the SemVer for you: any breaking change is a major bump, any new capability a minor, a fix-only release a patch. The working tree stays unversioned (ADR-19); the number appears only on the release artefact, generated, never edited."I stopped thinking about version numbers entirely," said Kira, a maintainer. "I write intents and mark each one additive, breaking, or fix. abcd cuts
v2.0.0when something actually breaks — and it will not let me ship a minor that quietly removed a command. The version finally means what SemVer promises, without anyone choosing it."
Why This Matters
A version number is not a decision to make — it is a fact about what changed. Asking a person to pick v1.4.0 forces release-thinking (which number?) onto what should be product-thinking (what does this feature do to compatibility?). abcd already makes the intent the unit of the why; the version should fall out of the intents in a release, not be authored beside them.
SemVer remains a real contract — consumers of the plugin rely on a major bump meaning "something broke". So the one irreducible input, is this change breaking?, is captured where the judgement naturally belongs: on the intent, once, when it is written. abcd aggregates those judgements at release time and mechanically guards against a mislabel, so the derived number keeps SemVer's promise without anyone thinking in versions.
What's In Scope
- An
impactclassification on every intent:additive(backward-compatible capability),breaking(incompatible surface or behaviour change), orfix(defect repair). Set once when the intent is shaped; it is a product judgement, not a version. - Version derivation in
/abcd:launch: gather the intents shipped since the previous release, take the highest-severityimpact, and compute the next SemVer — anybreaking→ major, else anyadditive→ minor, else patch. - A surface-diff guardrail:
launchsnapshots the/abcd:*command, flag, and manifest surface and compares it to the previous release. A removed or changed surface with nobreakingintent in the release fails the launch — a mislabel cannot silently ship a compatibility lie. - The derived version lands only on the release artefact — the
v*tag, the GitHub Release, and the marketplace manifest — consistent with ADR-19 (unversioned working tree) and ADR-20 (manifest lockstep).
What's Out of Scope
- Pre-1.0 and pre-release channel semantics (alpha/beta/rc suffixes) — a later refinement once the derivation is trusted.
- Per-consumer compatibility ranges or deprecation windows — downstream concerns, not the cut.
- Changelog prose generation — the changelog is auto-recorded from the shipped intents as a separate capability; this intent decides only the version number.
Scope Conditions
None stated.
Acceptance Criteria
BDD format, per
itd-1-acceptance-gates. These gates are checked byintent-fidelity-reviewerwhen this intent moves toshipped/.
- Given a set of intents shipped since the last release whose highest
impactisadditive, when/abcd:launchcomputes the next version, then the minor component is incremented and the patch reset (for examplev1.3.2→v1.4.0) with no human input. - Given at least one shipped intent declares
impact: breaking, whenlaunchcomputes the version, then the major component is incremented and minor and patch reset (for examplev1.4.0→v2.0.0). - Given the shipped intents are all
impact: fix, whenlaunchcomputes the version, then only the patch component is incremented. - Given the release removes or changes a
/abcd:*command or flag but no shipped intent declaresimpact: breaking, whenlaunchruns the surface-diff guardrail, then the launch fails with a report naming the changed surface — the mislabel is blocked, not shipped. - Given an intent is captured or shaped, when its
impactfield is absent or is not one ofadditive,breaking, orfix, theninternal/core/lintflags it as a blocker — every intent carries a valid impact before it can ship. - Given a release is cut, when the version is computed, then the number is written only to the release artefact (tag, GitHub Release, marketplace manifest) and the working tree carries no version, honouring ADR-19.
Open Questions
- Where does
impactlive for a bundle — on each member, or once on the bundle? Leaning: each member declares its own, and the bundle's impact is the maximum of its members, mirroring the release aggregation. - Does a
breakingdiscipline change — an acceptance gate that newly fails existing specs — count toward the version, or only surface and behaviour intents? Leaning: yes, a newly-enforced discipline that breaks consumers isbreaking. - Should the surface-diff guardrail also catch behavioural breaks behind an unchanged surface, or is that explicitly the author's
impactcall? Leaning: the surface-diff catches structural breaks only; behavioural breaks remain the intent author'sbreakingjudgement.
Audit Notes
Fidelity review — receipt rcp-0d18ea2d4682 (verifier intent-auditor claude-fable-5-1).
Provenance: intent-auditor@claude-fable-5-1 · rubric_hash sha256:effa65b3e9e88ff29433b443ec2be159522a8b0b71cf1434526514aa61edb13e · prompt_hash sha256:505eb999a5cd60215427c766ccdda96b4433fb36d00128305d7392a67a0a9417 Input attestations: diff:tree at de3ba5fa (spc-10 delivered; main after PR #661)@sha256:4e7430d38ef6b7b6bc533fdf0b8b32a6e08a3e2449d0566027d43316036b8178;
Acceptance rollup: MET 2 · MET_WITH_CONCERNS 4 · NOT_MET 0 · INCONCLUSIVE 0
Per-criterion verdicts:
- ac-1 — MET_WITH_CONCERNS: DeriveNext maps a highest impact of additive to minor++ with patch reset at >= 1.0 (1.2.3 -> 1.3.0 in the table test) with no human input; the concern is the pre-1.0 row, where additive bumps the PATCH (0.3.0 -> 0.3.1), and the repo sits at 0.9.0 so every cut to date has used that diverged row (ADR-37 as amended) evidence: internal/core/changelog/version.go:54 — "case bump == ImpactAdditive:" evidence: internal/core/changelog/version_test.go:31 — "{"stable additive bumps minor", "1.2.3", ImpactAdditive, "1.3.0", true}," evidence: internal/core/changelog/version_test.go:36 — "{"pre-1.0 additive bumps patch", "0.3.0", ImpactAdditive, "0.3.1", true}," evidence: internal/core/changelog/version.go:25 — "The pre-1.0 row is load-bearing, not a shortcut."
- ac-2 — MET_WITH_CONCERNS: a breaking impact bumps the major with minor and patch reset at >= 1.0 (1.2.3 -> 2.0.0 in the table test); the concern is the pre-1.0 row, where breaking bumps the MINOR and no input can ever derive 1.0.0 — the first stable version is an out-of-band human roll the verb does not offer evidence: internal/core/changelog/version.go:49 — "case bump == ImpactBreaking:" evidence: internal/core/changelog/version_test.go:30 — "{"stable breaking bumps major", "1.2.3", ImpactBreaking, "2.0.0", true}," evidence: internal/core/changelog/version_test.go:35 — "{"pre-1.0 breaking bumps minor", "0.3.0", ImpactBreaking, "0.4.0", true}," evidence: internal/core/changelog/version_test.go:66 — "func TestDeriveNextNeverDerivesFirstStable"
- ac-3 — MET: an all-fix cut increments only the patch component, at >= 1.0 and pre-1.0 alike, per the table test evidence: internal/core/changelog/version.go:57 — "default: // ImpactFix, the only remaining bump-driving member." evidence: internal/core/changelog/version_test.go:32 — "{"stable fix bumps patch", "1.2.3", ImpactFix, "1.2.4", true}," evidence: internal/core/changelog/version_test.go:37 — "{"pre-1.0 fix bumps patch", "0.3.0", ImpactFix, "0.3.1", true},"
- ac-4 — MET: GuardSurface diffs the current surface snapshot against the one at the last release tag; a break with no breaking record in the cut fails with a reason naming every changed command, flag and manifest entry, and the emit step turns that into a surface-guard refusal of the cut evidence: internal/core/changelog/guard.go:96 — "func GuardSurface(root string, current surface.Snapshot) (SurfaceGuard, error) {" evidence: internal/core/changelog/guard.go:137 — "g.Reason = failureReason(g.BaseTag, g.Breaks)" evidence: internal/core/release/emit.go:207 — "if guard.Status != changelog.SurfaceGuardPassed {" evidence: internal/core/changelog/guard_test.go:390 — "func TestGuardSurfaceReportsEveryBreak"
- ac-5 — MET_WITH_CONCERNS: intent_impact_valid is armed as a blocker in the real config and flags an invalid or internal impact in every bucket and an absent one in shipped/; the concern is that an ABSENT impact passes at capture and shaping (drafts/, planned/) — create.go leaves impact optional on a draft — so the bar bites at the move into shipped/, narrower than 'captured or shaped' evidence: internal/core/lint/lint.go:1754 — "func checkIntentImpact(tree intentTree, cfg RuleConfig) []Finding {" evidence: internal/core/lint/lint.go:1769 — "if r.bucket == "shipped" {" evidence: internal/core/lint/impact_test.go:71 — "func TestIntentImpactValid" evidence: internal/core/lint/impact_test.go:240 — "func TestImpactRulesArmedInRealConfig" evidence: internal/core/intent/create.go:225 — "impact is optional on a draft"
- ac-6 — MET_WITH_CONCERNS: the render stamps the derived number into the payload copies only and the working-tree manifests are proved unchanged and version-absent; the concerns are that the in-tree CHANGELOG dated heading does carry the version (ADR-37), and that the stamped marketplace manifest lives only in a payload staged with --payload-dir that release.yml never publishes — the tag and GitHub Release carry it, the marketplace manifest users install does not evidence: internal/core/launch/render_test.go:94 — "func TestRenderPayloadLeavesSourceTreeUnversioned" evidence: internal/core/launch/render.go:386 — "editManifest reads a manifest from the PAYLOAD (never the source tree)" evidence: internal/surface/cli/ship.go:99 — "func publishedVersion(repoRoot string) string {" evidence: .github/workflows/release.yml:327 — "gh release create "${TAG}" bin/abcd-* bin/checksums.txt"
Gap audit:
- honoured:
- every intent declares an impact of additive, breaking or fix, enforced by lint evidence: internal/core/lint/lint.go:131 — "intentImpactValues = joinImpacts(changelog.ImpactAdditive, changelog.ImpactBreaking, changelog.ImpactFix)" evidence: internal/core/lint/impact_test.go:240 — "TestImpactRulesArmedInRealConfig"
- the version is derived from the highest impact of the records shipped since the last tag evidence: internal/core/changelog/version.go:38 — "func DeriveNext" evidence: internal/core/changelog/impact.go:109 — "func MaxImpact(impacts []Impact) Impact {"
- a surface-diff guardrail blocks a mislabelled break and names the surface evidence: internal/core/changelog/guard.go:96 — "func GuardSurface" evidence: internal/core/changelog/guard_test.go:390 — "TestGuardSurfaceReportsEveryBreak"
- the working tree stays unversioned (ADR-19) evidence: internal/core/launch/render_test.go:94 — "TestRenderPayloadLeavesSourceTreeUnversioned"
- diverged:
- breaking -> major, additive -> minor — delivered with a pre-1.0 row (breaking -> minor, additive -> patch) that the criteria do not state and that every cut so far has used evidence: internal/core/changelog/version.go:21 — "prev is 0.x breaking -> minor++, patch = 0"
- impact flagged as a blocker when captured or shaped — an absent impact is flagged only in shipped/ evidence: internal/core/lint/lint.go:1769 — "if r.bucket == "shipped" {"
- the number lives only on the release artefact — the CHANGELOG dated heading in the tree is the carrier the cut reads evidence: internal/surface/cli/ship.go:99 — "func publishedVersion"
- missing:
- the marketplace manifest carrying the derived version as a published artefact — the stamped payload is optional and unpublished evidence: internal/surface/cli/ship.go:224 — "if payloadDir != "" && ingested.Written {" evidence: .github/workflows/release.yml:327 — "gh release create "${TAG}" bin/abcd-* bin/checksums.txt"
Frontmatter
| id | itd-73 |
|---|---|
| type | intent |
| lifecycle | shipped |
| kind | standalone |
| severity | minor |
.abcd/development/intents/shipped/itd-73-derived-versioning.md
open on GitHub ↗ · commit history ↗
Referenced by12
- adr-31The release version is derived from the intents in it, never authoredrelated
- adr-33Launch phase ownership is tiered; the phase index is the sole ownership sourcerelated
- adr-37Releases are changelog-driven — rolling `[Unreleased]` is the release decision, and automation tags exactly that commitrelated
- iss-2610262033180356itd-73 derives a major bump from any breaking intent; 04-launch says no set of records derives 1.0.0 from a 0.y.z baserelated
- iss-2610262035168556itd-73 derives the release version from the shipped intents alone; the capture chapter says a resolved issue's impact drives the derived version toorelated
- iss-2610262042183674itd-73 makes any breaking intent a major bump and a cut with nothing additive a patch; the version entry bumps the minor for a breaking change before 1.0 and derives no version for an empty cut.related
- iss-2610262045379282itd-67 picks the bump tier by phase-completion detection and keeps the version in plugin.json; itd-73 derives it from shipped intents' impact on an unversioned treerelated
- iss-2610262045391451itd-2609231013154443 excludes intents with impact: internal; itd-73 defines impact as additive, breaking or fix onlyrelated
- itd-60When A Surface Ships, The Brief Describes It — Or The Intent Does Not Reach Shippedbuilt on by
- itd-2609212103572513An intent names the release it must land by, and the cut says whether it didrelated
- itd-2609231013154443Every release arrives with a press release composed from what shippedbuilt on by
- spc-10derived-versioningimplemented by
mentions18
- 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-20The two release manifests stay version-consistent by an anti-drift checker over a pinned path list; the source view stays unversioned; `--allow-dirty` must never bypass it
- dcn-26071700000000052026-07-17 — Release burst (maintainer-directed): adr-37 adopts
- dcn-26072100000000022026-07-21 — itd-67's "a phase completed since the last launch -> minor" bump
- dcn-26072400000000032026-07-24 — itd-93 design settled in maintainer grill: (a) surface is a
- dcn-26080200000000032026-08-02 — iss-38 (v0.5.0 item D): hand-maintained indexes are either DELETED or GATED, never corrected and left hand-kept, because a corrected list drifts again on the next commit and nobody notices the second time either (adr-5, derive don't store). The ledger named four stale indexes; three were real and one does not hold. (1) `intents/README.md` transcribed four lifecycle directories as ASCII trees and had fallen sixteen drafts, one promotion (itd-73, listed as a draft while living in `planned/`), two disciplines and two superseded intents behind, with a `shipped/` section still calling the directory empty over thirteen shipped intents — deleted, because the enumeration carried nothing the directory does not carry better, and what a filesystem cannot state (the kind taxonomy, the lifecycle contract, the bundle history) stays. (2) `commands/README.md` (fourteen verbs named, eighteen files present) and (3) `internal/README.md` (core described as "identity/version and the read-only status snapshot" while its own later bullets described twenty packages, and `adapter/scanner` filed under planned seams although it ships with its own pattern set, redactor and tests) keep short, high-value enumerations — so those ship WITH the detector rather than as a promise to remember. The new `index_drift` rule holds a marked region (`<!-- index: <id> -->`) to the directory it enumerates in one of two modes: `exact`, where listing and directory must agree both ways, and `absent`, where every listed path must still be missing — the planned-seams shape, whose drift is a seam that shipped. Scoping is an explicit marker plus a configured entry pattern rather than a markdown-list parser: one generic rule, no bespoke parser per README, and unrelated prose beside a list cannot fire it. It fails closed three ways (region gone while the config entry remains; region parsing to zero entries; malformed spec), so half-deleting an index is a finding rather than a silent disarm. (4) The fourth instance — "the repo README Layout omits `skills/` from the plugin surface" — was investigated and does NOT hold: there is no `skills/` directory, abcd ships zero skills deliberately (the three workflows once shaped as skills became commands, closing iss-61; `brief/05-internals/08-skills.md` states it), so the Layout section is correct by omission and was left untouched. Inventing a `skills/` reference to satisfy a finding would have written a falsehood into the record to close a ticket — the same disposition iss-37 reached on its one non-holding instance. CHANGELOG: an entry, unlike iss-34's test-only pass, and impact `additive` rather than `internal` — the rule lives in the shipped lint engine that `abcd docs lint` runs, so any abcd-managed repo gains it by declaring its own pairs in `.abcd/docs-lint.json`.
- dcn-26080200000000092026-08-02 — iss-41 (v0.5.0 item D): the interim delivery-state rule is stated as the one the lifecycle already implies — **delivered capability is represented by the intent leaving `drafts/`, and nothing else says it** — because the alternative on offer (a new tier, or a per-intent "partly shipped" field) invents lifecycle vocabulary to describe a citation problem. The ledger's premise that `shipped/` is empty has aged out: thirteen intents live there now, so the mechanism exists and what was missing was the gate holding the changelog to it. `delivery_state` reads each version entry's delivery sections (`Added`, `Changed`; a repo's configured list is unioned with those, never substituted for them) and blocks an `itd-N` citation whose intent is still in `drafts/`. Scoping by section rather than by whole entry is what keeps it precise: the ids under `Fixed` in this corpus are provenance for a defect — which draft two branches minted at once — and blocking them would force either a falsehood or a deletion of real information. It fails closed three ways: no changelog, no intents store, and a store holding no lifecycle bucket — the last being a root pointed one level too deep, which resolves every citation to nothing and reads exactly like a clean corpus. The corpus had TWO instances, not the one filed: itd-60, whose deterministic layer shipped as `abcd docs lint` while its semantic layer is still five open questions, and itd-85, which shipped WHOLE as `abcd audit` and was simply never promoted. They get different treatments because they are different facts. itd-60 stays in `drafts/` and loses the citation — an intent is delivered whole or not at all (split-the-intent doctrine), so citing a partial delivery reads as a promotion that never happened, and splitting itd-60 to make the citation true is editorial work this issue does not carry. itd-85 loses the citation too, but under protest: the honest fix is promotion, and promotion is blocked on the `shipped/` schema wanting a non-null `spec_id` the audit verb has never had. Filed as iss-180 rather than papered over or forced — writing a retroactive spec to satisfy a lint is the failure mode this repo keeps naming. The out-of-scope half is a REUSE, not a new rule: `index_drift` gained one field, `dir_entry`, the mirror of `entry` on the directory side, which reduces a filename stem to the id a document actually enumerates. Without it a hand-written listing can only agree with a directory by transcribing whole slugs, which is a listing nobody writes, and the alternative was a second rule shaped exactly like the first. The list itself had drifted nineteen entries while claiming in its own prose to be derived and "not hand-counted": itd-47, itd-73 and itd-74 had left `drafts/` for three different buckets, and sixteen captures were missing. Regenerated, wrapped in the marked region, and the sentence promising hand-kept lockstep is replaced by the gate that enforces it. The four later-phase items that never had an intent id move OUT of the derived list — a derivation cannot produce them, so their presence was what made the claim false in the first place.
- dcn-26092800000000082026-09-28 — Correction to the 2026-09-25 entry on the build's open-question check (lane implementer, autonomous run A, lane drainInt, on iss-2609260932374727). A settled LABEL (`resolved:`, `RESOLVED:`, `Deferred:`) is no longer read anywhere in the item: it counts opening a line of the item (its first line or a continuation line), after a closing bold (`**Which surface scaffolds it?** RESOLVED:`), or after a dash (`**Refusal breadth** — resolved:`, `**Relationship to itd-73** (derived versioning) — RESOLVED:`). The same word and colon mid-sentence are prose, so "Which id wins once the split is resolved: the old or the new?" is a question, where the entry's "anywhere in the item" read it as settled and let build start past it. The bold-span marker keeps its reach anywhere in the item. Every intent in the tree reads the same open-question count under the tightened rule as under the old one, so no record changes verdict.
- 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-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).
- iss-2610021502084298No check meets the retired role word in an intent's press release at the record: the docs-lint rule roles/retired-role-word roots at the docs, the command pages and the rules, so a shipped or planned intent's press release carrying it is refused only when the site composer renders it as the landing page's hero quote, which is how itd-60's first MET audit broke the site-render gate (iss-2610021446271464). After the R5 reword of itd-60, thirteen shipped press releases still carry the word (itd-65, itd-66, itd-67, itd-69, itd-73, itd-74, itd-93, itd-102, itd-111, itd-114, itd-125, itd-2609111003026787, itd-2609212137129937), so the next MET audit ingested for any of them refuses the render again on a branch that only recorded an audit; the product thinker's ruling R5 (2026-10-02) accepted that a future slip breaks the build and did not ask for this check. Related to iss-2610020731591808, which names the brief, the principles and the personas registry as swept but unguarded.
- itd-1Every Intent Has Acceptance Criteria; Every Audit Verifies Them
- itd-93abcd Scaffolds a Release Gate That Works on the First Try
- itd-105A fresh plugin install just works: the hook binary survives install and every update
- itd-108The plugin installs from the curated release artifact, not the repo, and every cut release reaches users automatically
- spc-11installable-versioned-plugin
- spc-14abcd-scaffolds-a-hardened-changelog-driven-release-gate-into
- spc-2610100613109045a-person-who-types-abcd-sees-only-the-commands-people-use