Repository navigation
v0.120.0
The version-fate model (recorded / unrecoverable / never-released), the release backfill promoted to a first-class command, operator-declared transition facts, and fleet-wide strictness.
Context
This release ships one campaign: every version number or tag a repository
carries now has exactly one declared fate, and every surface that reads or
repairs release history answers from the same authorities.
The release archives gained a three-state fate model -- recorded (the
release commit and trees), unrecoverable, or never released -- with a new
shipped_as field carrying the historical tag spelling a version actually
shipped under. The old backfill maintenance script is retired in favor of
rlsbl release backfill (consequential, preview/apply, with a
description-recovery chain and a hard refusal on unexplained tags), and
rlsbl transition record is the typed door for the two operator-declared
history facts. changelog remove completes the changelog surface.
Strictness grew in the same direction: unknown keys on the policed
configuration surfaces are refused at load, the dev_node alias is gone,
explicitly-empty flag values are refused centrally, a present git stash
blocks the guarded operations, and consequential classification now means
exactly one thing -- only a human may authorize the act.
Three banned vocabularies were renamed throughout: the release record
(formerly "ledger"), the transition record (formerly "lineage"), and the
release commit family (formerly "anchor"), including the archive marker
key rename to unrecoverable.
Breaking
- Renamed: the release ledger is now the release record. The
rlsbl.ledgermodule is nowrlsbl.release_record, and every error message, check result and doc page that said "ledger" now says "the release record" or "the release archives". - Renamed: lineage records are now transition records. The committed repository-surgery log is written to
transitions.jsonl(waslineage.jsonl), therlsbl.lineageandrlsbl.lineage_followupmodules are nowrlsbl.transition_recordandrlsbl.transition_record_followup, and the mirror reconciler's separate git-ancestry verdict is nowancestry-undetermined(waslineage-undetermined). - Renamed: the release anchor is now the release commit. Every message, check result, JSON field and doc that spoke of a release "anchor" now says "release commit":
rlsbl status --jsonandrlsbl unreleased --jsonreturnnearest_release_commit_tag/nearest_release_commit_version(wererange_anchor_tag/range_anchor_version), an archive is "recorded" or "unrecoverable" rather than "anchored" or "unanchorable", therlsbl.anchor_remapmodule is nowrlsbl.release_commit_remap, and the mirror's per-version tag verdictunanchoredis nowunderivable. The key inside archived release files was renamed too --unanchorablebecameunrecoverable-- as part of the version-fate work shipping in this same release. - Version fate is now a three-state model. An archived release file records exactly one of: the release commit it shipped from,
unrecoverable = true(it shipped, from a commit nothing can name), or the newnever_released = true(the version number exists but no release ever used it). The new optionalshipped_asrecords the historical tag spelling a version actually shipped under when it differs from the scheme in effect today. Breaking: the marker key is spelledunrecoverable; the oldunanchorablespelling is refused as an unknown key, so an archive carrying it must be renamed. - The transition record gains two event kinds, and its rewrite kind is renamed.
release-history-closedrecords that a member's or releasable's release history is deliberately closed, andnon-version-tagrecords one tag deliberately outside the version model; both carry an operator reason. Breaking: the rewrite event's kind literal isrelease-commit-remap, and the oldanchor-remapspelling is refused as an unknown kind. - The two backfill scripts are retired.
scripts/backfill_release_anchors.pyandscripts/set_archived_descriptions.pyare removal stubs that exit 1 namingrlsbl release backfill(and its--overridesfile, which replaces the JSON description map). Every error that used to print a hand-written multi-step archive-recovery procedure now names the command instead. - The remote-release dispatch feature is removed.
rlsbl scaffoldno longer renders arelease-dispatch.ymlworkflow, and theremote_releaseconfig key does nothing. Delete the workflow file from any repo that has one. - Unknown keys are refused at load. A key rlsbl does not know on a workspace.toml member table, releasable table or top level -- or in a standalone repo's
.rlsbl/releasable.toml-- is now a hard error naming the surface, the key and the file, instead of a line that was written and never read. The retireddev_nodemember key is deleted: declare a dev node withdev_only = trueplusreleasable = false, which its refusal message spells out. - An empty flag value is refused instead of read as absence. Passing
--name ""(or any named string flag with an empty or blank value) is now a hard error naming the flag. Most consequentially,rlsbl monorepo release init --releasables ""used to scaffold a section for EVERY releasable; it now refuses. Omitting a flag is unchanged. - Two managed-repo states are now refused. A repository with a git stash is a hard error on
release run,release resume,release reconcile --applyandrelease backfill --apply(--allow-dirtydoes not waive it), and the newstash-freeproject check reports it earlier; drop the stash after landing its work. A workspace member outside every releasable that carries its own.rlsbl/changes/is a hard error wherever that directory would be read -- changelog-directory enumeration,rlsbl monorepo status, and pre-push coverage. rlsbl monorepo rename-releasablenow asks before it runs. The rename declares a boundary-alias event in the releasable's transition record, pushes an alias tag to origin, and changes the tag scheme every future release of that releasable uses -- decisions only a human may authorize. The command is nowconsequential: an interactive run prompts, and a non-interactive one must pass--approve-consequential.
Features
- A version recorded
never_releasedkeeps its changelog section, annotated as never released. Such a version can carry finalized changelog files -- entries written and locked before the release was abandoned -- so the section is rendered in full with its own archived description, above a line stating that no release was ever published under that version number. - New command:
rlsbl release backfill. It brings a repository's release archives into the three-fate model from its real history: it records each version's release commit from its tag (or from the historical spelling the archive names inshipped_as, or from the version-bump commit), completes an archive missing any required field, materializes an archive for a released version that never got one, and adopts a version tag no store records as the release it is evidence of. A reconstructed description comes from the first source that yields one -- an operator-reviewed--overridesfile, the version's GitHub Release body, its CHANGELOG.md section, the commit subjects in its tag range -- and the archive names the source it came from. Tags the repository cannot account for are listed first and refuse the whole apply, with each of the three cheap resolutions spelled out. docs/release-workflow.mddocuments the backfill as a command. The backfilling section now describesrlsbl release backfill: its five verdicts, the recovery chain for a reconstructed description, the one fate it will not derive (never_released, which you declare by writing the archive), and the unexplained-tag refusal with its three resolutions.rlsbl release backfill --dry-runexits non-zero when it finds a tag the repository cannot account for, after rendering the whole plan -- the same shaperlsbl release reconcile --planhas. A preview that returned 0 would report a repository as accounted for when the apply would refuse it. A stash is reported by the preview and still refuses only the apply.rlsbl transition recordis the typed door onto the two transition-record facts an operator declares.--non-version-tag <tag>declares a tag that stands outside the version model;--release-history-closed <subject>declares a member's or releasable's release history deliberately over. Exactly one must be elected,--reasonis required, the event is appended to the repository-scoped transition record and committed, and a second declaration of the same kind about the same subject is refused naming the one already recorded.rlsbl release backfillstops listing a declared tag as unexplained andrlsbl release reconcilestops owing a verdict on it.- A renamed project's past versions get their current-spelling tags back. The refs a released version owns now include the historical tag spelling its release archive records in
shipped_as, alongside the aliases the transition record carries.rlsbl release reconciletherefore mints the current scheme's spelling at that version's release commit while the old-spelling tag stands explained, and theunpublished-refscheck looks for both. When an alias event and ashipped_asfield disagree about one version's spelling, the ref set is a hard error naming both sources -- rlsbl never guesses which record is right. rlsbl monorepo listshows the member facts its help promised. Each row now carries the releasable a member is versioned under and the member flags it declares (library, dev-only, test-only), not just name and path.rlsbl changelog remove. Deletes one changelog entry fromunreleased.jsonlor a released version's JSONL, addressed by its ULID identifier or by the commits it covers. A released file is unlocked, rewritten, re-locked, and followed by a CHANGELOG.md regeneration and a GitHub Release notes sync. A selector matching several entries -- or none -- is refused with every match named, and nothing is written.- Longer help for four flags.
release scrub's--pattern,--fileand--from-commit, andrewrite go-module-path's--to-module, each carried a help line short enough that the documentation generator flagged it. All four now say what the value is and what happens to the repository when it is supplied. - Documentation for entry removal and the scope distinction. The JSONL changelog page gains a removal section and lists
changelog removeandchangelog remapin its command table; the check tables describe what the range and orphan checks now report; and the repair-surface lists that claim to be complete namerlsbl changelog remove. releasable-residuenow also reports release state on a member that releases nothing -- a dev node, or any member declaredreleasable = false-- covering release archives, a changelog directory, and version tags in the member's own scheme. Arelease-history-closedtransition record event naming the member silences it, so a member that deliberately stopped releasing keeps the record of what it released.
Fixes
- A version recorded
never_releasedis no longer mistaken for a release. It is skipped by the latest-release fact, the unreleased range, and the checkout-contains-latest refusal.rlsbl release undodescends past it to the real latest release instead of selecting the phantom and dying on its commitless archive; theunpublished-refscheck stops demanding refs and a GitHub Release for it; andrlsbl release reconcileskips it while claiming the refs it would have owned, so a tag carrying its name can no longer fire the tripwire.rlsbl status --jsongainslatest_release_stateandnever_released_versions. - A release archive that cannot be read now stops changelog generation instead of silently emptying a version's description. The archive read behind every regenerated section went through a raw parse that turned bad TOML, a missing
format_versiongate or a wrong-typed field into an empty description -- whichrlsbl changelog generatethen committed over the version's real prose in both CHANGELOG.md and the read-only per-version.md. It goes through the validating reader now and hard-errors naming the file. An absent archive is still empty metadata, as before. - Archive writers refuse a second version fate. Recording a release commit on an archive that already says
unrecoverableornever_released-- or marking one unrecoverable that saysnever_released-- is now a hard error naming the file and the conflict, instead of silently producing a two-fate archive that every later read rejects. - The release-archive backfill no longer corrupts a never-released archive.
rlsbl release backfillrecognizes thenever_releasedfate as settled and proposes nothing for it, instead of planning anunrecoverablemarker beside it -- so a repository carrying a phantom version passes the backfill unchanged however many times it runs. - The mirror plan names a never-released version for what it is. A version whose archive records
never_releasednow gets its ownnever-releasedverdict (nothing shipped under that number, so there is nothing to mirror) instead ofunderivable, which told the operator to go restore a release commit that never existed. rlsbl release resumere-checks the release file. A hand-authoredcandidate_sha,tree_hashes,unrecoverable,never_releasedorshipped_asinunreleased.tomlnow aborts a resume before the mutating phase, the same way it abortsrlsbl release run-- previously a resume archived it as the version's permanent record.- Documentation: the release-workflow page describes the flow-owned field set by reference to its one authority (rather than as a closed pair), states that the backfill pass is idempotent over a never-released archive too, and the reconcile helper's docstring names all three version fates.
- A never-released pre-release is annotated in CHANGELOG.md. When a stable version's section consolidates its pre-release cycle, a pre-release whose archive records
never_releasednow carries the never-released note in its own sub-section, instead of reading as a release that happened. - The archived description for 0.3.1 (and the CHANGELOG.md section generated from it) says its archive records no release commit, in the current vocabulary, rather than naming the retired "anchor".
rlsbl release reconcileno longer fires its tripwire on a tag the transition record declares anon-version-tag. A nightly marker or an imported vendor tag recorded that way is an explained ref: it owns no verdict, and a divergence on it no longer aborts every reconcile on the repository.- Non-version tags recorded in a monorepo are now seen. A tag declared with
rlsbl transition record --non-version-tagis written to the repository-scoped transition record;rlsbl release reconcilein a workspace read only the releasable's own record, so it kept firing its publication tripwire on the very tag the declaration silences. rlsbl release backfillaccounts for every spelling of a released version's refs. It asksexpected_refs-- the single authority -- instead of rendering one tag format of its own, so a monorepo's Go companion tags and a second live spelling minted beside an archive'sshipped_asare explained rather than refusing the whole apply.rlsbl release backfill --overridessettles. An overrides file carrying acontextplanned a write on every run, even when the archive already recorded exactly that context; it is now compared like the description and left alone when it agrees.- A releasable's tagless version is no longer recorded unrecoverable.
rlsbl release backfilllooked for a version-bump commit under the standalone message shapes only, so a monorepo releasable's bump commit ({name}: release v{version}) was never found and the version got a permanentunrecoverablemarker even though its commit was in the history. rlsbl release reconcilestates the refusal instead of crashing. When two records disagree about the tag spelling a version shipped under, the ref set cannot be derived; the refusal now arrives as the command's own error naming both records, where it used to escape as a traceback.rlsbl release undono longer deletes the tag a version shipped under. A version whose archive recordsshipped_asowns that historical spelling as a recorded alias; undo treated every non-primary ref as a companion of the release it was undoing and deleted it locally and on origin. The spelling consumers already resolve now stands where it is.- The backfill and reconcile docs describe what the commands now do. The tag-spelling derivation, the unexplained-tag rule, the scopes a backfill walks, the alias sources
unpublished-refsrenders, and whatrlsbl release undoleaves alone are all restated against the shipped behavior. - Empty positional arguments are named as arguments.
rlsbl monorepo add ""and the other positional-taking commands used to refuse with--path was given an empty value ... drop --path, naming a flag that does not exist. The refusal now names the argument and asks for a real value. - An explicitly-empty CLI value is never read as absence. Every remaining flag and argument that read a supplied-but-empty value as absence now refuses it -- most consequentially
rlsbl release undo --version "", which used to undo the LATEST release, andrlsbl changelog edit --description "", which reported success having written nothing (the refusal names--unset-descriptionfor clearing the field). - A supplied-empty changelog flag is reported as empty.
rlsbl changelog add --commits "",--description ""and the same flags onchangelog amendsaid the value was missing;changelog remap --map-file ""said no map source had been named. All of them now name the empty value that was actually passed. - The monorepo guide's member-field table no longer instructs you to break your workspace. It documented a
targetkey thatworkspace.toml's loader refuses, and omittedimport_name,test_onlyandlint_allow. The table now matches the loader's key set exactly (a test binds the two), and the "What the loader refuses" section lists every refusal in the order the loader reports them. - The
config-schemacheck's documented behavior matches what it does. The checks reference claimed it refuses unknown keys and wrong types in.rlsbl/config.json; it refuses the retiredprivatekey, an emptytargetslist and arelease.modekey, and validates thepipelinessection -- unknown keys there are not policed. rlsbl monorepo extractno longer drops a member's own declarations. A multi-member extract wrote the destination workspace from a hand-listed key set, silently losingdev_only,description,test_onlyandlint_allow-- so an extracted member was lint-checked under rules it never declared. Every key the extract does not rewrite itself now travels with the member.- The documented
workspace.tomlexamples are ones the loader accepts. The monorepo and conversions guides showed[[projects]]blocks carrying atargetkey, whichworkspace.toml's loader refuses -- copying either example produced a workspace that would not load. - Printed remedies are runnable. The changelog orphan finding named
rlsbl changelog edit --commits <hash> --remove, a flag no command declares; it now names a realrlsbl changelog removeinvocation carrying the entry's own identifier. The name-availability refusal's usage line namedrlsbl check, which takes no package names, and now namesrlsbl check-name.rlsbl status's commits-ahead hint named the barerlsbl releasegroup and now prints the whole invocation, required flags included. - Cross-filed changelog entries are named for what they are. In a workspace, an entry whose commit belongs to another releasable's territory was reported by the
changelog-rangecheck as "not in unreleased range" -- false, since the commit is in the range and only its owner is wrong. It now gets its own finding, naming the owning member and releasable and printing therlsbl changelog removeplusrlsbl changelog addinvocations that move the entry. The orphan check counts and names the same distinction. rlsbl statusat a workspace root says what to run instead. Standing at a workspace root whose root member declares no release target, the command reported "no package.json, pyproject.toml, or go.mod found" -- true of the directory and useless to the reader. It now says the root member declares no target and namesrlsbl monorepo status. A root member that does declare one is a real project and still resolves.dead-modulesnow analyses a Flutter app fromlib/main.dart. A Flutter app has neither alib/<package>.dartbarrel norbin/scripts, so the analysis previously found no entry point, traversed nothing, and reported the project clean.rlsbl changelog editandrlsbl changelog removeno longer narrow a changelog file's permissions. Rewriting an entry pinned the file to owner-only 0600, so an ordinary 0644unreleased.jsonl-- or an unlocked released file -- silently became unreadable to everyone else the first time an entry was edited or removed.changelog-rangeno longer points at a directory that does not exist. When an entry names a commit whose every file is rlsbl's own bookkeeping, no member owns it, so the finding now says the files are tool-owned and need no coverage anywhere and prints only the removal -- instead of telling the reader to re-file the entry from the owning member's directory.- Rewritten files keep their permissions. Bumping a version no longer narrows
.strictcli/schema.json,.rlsbl/config.json, a releasable'sversionfile,selfdoc.jsonor an Xcode project file from 0644 to owner-only 0600. changelog addno longer sends you to a directory that does not exist. When a refused commit touches only tool-owned files (a regenerated snapshot, a generated workflow), the error now says the commit is exempt from changelog coverage and needs no entry anywhere, instead of pointing at an owning member's directory that has no owner; when another member does own the files, the message names it.