Documentation
¶
Overview ¶
Package release is the transport-agnostic composition of a release cut: the deterministic half of `abcd launch ship` and the whole of `abcd changelog`.
It is a composition, not a new domain. The version arithmetic, the record set-difference, the tag anchor and the surface guardrail all live in internal/core/changelog; the record-tree traversal lives in internal/core/lint. What is here is the ORDER those parts run in for a release cut, the refusals that stop one, and the one result value a host needs to compose the changelog prose. It sits in its own package because it reads both of those packages and changelog cannot import lint (lint already imports changelog for the impact enum).
Nothing here writes: the emit step is the input to a review, and the CHANGELOG heading is written only by the ingest step that follows the composer.
Index ¶
- Constants
- Variables
- type ChangelogEntry
- type ChangelogPayload
- type Cut
- type Entry
- type Headline
- type IncompleteError
- type IngestResult
- type PageResult
- type PayloadRefusal
- type PressReleasePayload
- type Quote
- type Reason
- type ReasonCode
- type ReceiptsProtocol
- type Refusal
- type RefusalKind
- type Section
- type UndoPlan
Constants ¶
const ArchiveDir = ".abcd/development/releases"
ArchiveDir is where a replaced page moves, named after the release its heading names. It is the permanent-record tier: the pages are written by the cut, never by hand.
const ChangelogSchemaVersion = 2
ChangelogSchemaVersion stamps the composed-changelog payload. It is versioned independently of every other abcd artifact (mirroring the synthesis schemas) so a future breaking change to the shape is detectable rather than silently misread by a composer written against the old one.
Schema 2 adds the release page (press_release). A schema 1 payload is refused as unsupported: the prompt and the binary ship together in the plugin, so no composer is left writing the old shape.
const MaxPayloadBytes = 1 << 20 // 1 MiB
MaxPayloadBytes caps the untrusted composed-changelog payload, mirroring the synthesis cap. It is exported so a front door can bound its READ at the same ceiling the core enforces — the front-door bound is a convenience, this one is the guarantee.
const PageFile = "RELEASE.md"
PageFile is the release page, at the repository root beside CHANGELOG.md.
Variables ¶
var ReasonCodes = []ReasonCode{ ReasonPayloadOversize, ReasonMalformedJSON, ReasonUnknownField, ReasonTrailingData, ReasonSchemaVersion, ReasonPromptVersion, ReasonStaleCut, ReasonTextOversize, ReasonMalformedID, ReasonNoCitation, ReasonEmptyProse, ReasonNoEntries, ReasonSection, ReasonSectionNotWritable, ReasonChangelogMissing, ReasonChangelogInvented, ReasonChangelogInternal, ReasonMissing, ReasonOutsideSet, ReasonDuplicateCitation, ReasonNoHeadline, ReasonPageForEmptySet, ReasonHeading, ReasonFence, ReasonBlockquote, ReasonQuoteSource, ReasonQuoteNotVerbatim, ReasonOutboundPolicy, ReasonPersonaRegistry, ReasonPrivacy, }
ReasonCodes is every code, in the order above. commands/launch.md is pinned to it, so the host's documentation of the loop cannot fall out of step with the refusals the binary makes.
Functions ¶
This section is empty.
Types ¶
type ChangelogEntry ¶
type ChangelogEntry struct {
// Section is the Keep-a-Changelog section this line belongs in.
Section Section `json:"section"`
// Records are the record ids this line reports. A line may report more than
// one record (a bundle shipped as one user-visible change), and a record may
// be reported by more than one line; what the bijection requires is that the
// SET of ids cited across the document equals the cut's required set.
Records []string `json:"records"`
// Text is the wording, and the wording only: the citation suffix is appended
// by the core, so prose carrying its own "(itd-73)" will read it twice.
Text string `json:"text"`
}
ChangelogEntry is one composed changelog line — the untrusted input shape.
type ChangelogPayload ¶
type ChangelogPayload struct {
// SchemaVersion must equal ChangelogSchemaVersion.
SchemaVersion int `json:"schema_version"`
// PromptVersion is the composing agent prompt's semver (itd-5).
PromptVersion string `json:"prompt_version"`
// NextTag is the version the composer was given, echoed back. It must equal
// the version this cut derives: the emit step and the ingest step are two
// separate reads of a moving repository, and a mismatch means the record set
// changed underneath the composer — prose written against a stale cut would
// pass the bijection only by accident.
NextTag string `json:"next_tag"`
// Entries are the composed lines, in the order they should appear within
// their section.
Entries []ChangelogEntry `json:"entries"`
// PressRelease is the release page, null or absent exactly when the cut's
// press-release set is empty (a fixes-only release has no page).
PressRelease *PressReleasePayload `json:"press_release,omitempty"`
}
ChangelogPayload is the whole untrusted host-composed document.
type Cut ¶
type Cut struct {
// Ready reports that the cut may proceed to the composer. It is exactly
// "no refusals", so a caller can never read a ready cut off a refused one.
Ready bool `json:"ready"`
// BaseTag is the release tag the cut is measured from, "" when none resolved.
BaseTag string `json:"base_tag"`
// NextTag is the derived version as a git tag, "" when nothing is released.
NextTag string `json:"next_tag"`
// Bumped reports whether the cut moves the version at all.
Bumped bool `json:"bumped"`
// Impact is the strongest impact in the cut — the judgement that decided
// the version.
Impact changelog.Impact `json:"impact"`
// DecidedBy names the records carrying that deciding impact, so the version
// can be traced to the record that caused it rather than asserted.
DecidedBy []string `json:"decided_by"`
// Added and Removed are the cut, split by direction. A record that LEFT a
// terminal folder is a user-visible change too, so it travels rather than
// being dropped.
Added []Entry `json:"added"`
Removed []Entry `json:"removed"`
// Guard is the surface-break guardrail's verdict on this cut.
Guard changelog.SurfaceGuard `json:"guard"`
// Findings is the unfixed-findings guardrail's verdict: what this cycle
// captured and has not answered. It travels on a PASSING cut too, because a
// waived finding is only consciously deferred if the release report says
// what was deferred and why.
Findings changelog.FindingGuard `json:"findings"`
// Targets lists every planned intent that names a release it must land by
// (itd-2609212103572513): targeted and unshipped. It is a report, never a
// refusal (adr-2609212115255771, decision 3), so it travels on a ready cut
// and a refused one alike.
Targets []launch.TargetedIntent `json:"targets,omitempty"`
// TargetsError is why the intent store could not be read for the list,
// empty when it was. It is reported rather than raised: a report that
// failed the cut would make the one field ruled never to refuse a refusal.
TargetsError string `json:"targets_error,omitempty"`
// Moves is every target in Targets this cut passes without shipping it —
// launch.MissedTargets over Targets and NextTag — each of which the ingest
// rewrites to `next` and names in the dated section. The dry run carries
// it so a reader sees the moves before anything is written
// (iss-2610020718369838), and the ingest moves exactly this list, so the
// two cannot disagree. A refused cut derives no tag and moves nothing.
Moves []launch.TargetMove `json:"target_moves,omitempty"`
// Refusals is every reason the cut cannot proceed, in the order they are
// checked. All of them are reported, not just the first: an operator fixing
// a release should see the whole list in one pass.
Refusals []Refusal `json:"refusals,omitempty"`
}
Cut is the emit step's result: the deterministic release cut.
It is the input to the changelog composer and, after it, to the ingest step that writes the dated heading — so its shape is a contract between three stages. A refusal is carried as a VALUE, in the same shape as changelog.Derivation and launch.RetentionPlan: "this cut cannot proceed" is a legitimate result a read-only preview must render, and errors are reserved for "the repository could not be read at all".
func Emit ¶
Emit computes the deterministic release cut for the repository at root and writes nothing.
current is the caller's view of the public command surface. It is passed in rather than built here for the reason changelog.GuardSurface states: building it means walking the cobra tree, which internal/core must not do. The front door owns the walk; this owns the judgement.
The order is deliberate. The derivation runs first, and if it refuses this returns THAT refusal alone: its four refusals (no tag, release in flight, an unlabelled record, a record move left uncommitted) all mean the anchor or the record set cannot be trusted, and every later check reads the same inputs — so continuing would restate one fault as three and send the operator hunting for bugs that are not there. Once the derivation holds, the remaining checks all run and accumulate, because they are independent and an operator fixing a release should see the whole list at once.
type Entry ¶
type Entry struct {
// ID is the record id (itd-N, iss-N) — the token every generated changelog
// line must cite, and the key the completeness bijection is computed over.
ID string `json:"id"`
// Path is the record's repo-relative path, so a reviewer can open it.
Path string `json:"path"`
// Impact is the record's declared product judgement.
Impact changelog.Impact `json:"impact"`
// Title names the record; never empty.
Title string `json:"title"`
// Summary is the record's opening paragraph — source material, not the line.
Summary string `json:"summary"`
// InChangelog reports whether this record must be cited in the prose. It is
// carried per entry rather than left for the host to re-derive from Impact,
// because the bijection and the version must agree on one definition of
// "user-facing" and the host is not the place to keep that rule. It is
// changelog.Record.InChangelog verbatim, which is why a record with no valid
// impact reads true here: unknown is not internal.
InChangelog bool `json:"in_changelog"`
// ShippedInErr is why this record's `shipped_in:` did not parse, empty when
// the field is absent, null, or valid. A record carrying one is IN the cut —
// an unreadable value never removes a record — so this is the only place an
// operator learns that a record tried to say it shipped elsewhere and failed.
//
// Reported rather than refused. Refusing would block a release on a typo in a
// field whose whole purpose is to take records OUT, and the fail-safe
// direction already holds: the record stays, so the worst outcome is a
// redundant changelog line rather than a missing one.
ShippedInErr string `json:"shipped_in_err,omitempty"`
// InPressRelease reports whether the release page must cite this record:
// changelog.RecordSet.PressReleaseRequired, carried per entry for the reason
// InChangelog is — the host must never re-derive the rule.
InPressRelease bool `json:"in_press_release"`
// contains filtered or unexported fields
}
Entry is one record in the cut, carrying exactly what a changelog composer needs to write a line about it and nothing more: what it is, where it lives, what shipping it did, and the author's own words to draw on.
type Headline ¶ added in v0.10.0
type Headline struct {
Records []string `json:"records"`
// Text is the wording only; the citation suffix is the core's.
Text string `json:"text"`
}
Headline is one prose paragraph and the intents it tells.
type IncompleteError ¶
type IncompleteError struct {
// Missing are required records no line cites — the omission that would make
// the release record silently untrue.
Missing []string
// Invented are cited ids that are not in the cut at all — a line about
// something that did not ship.
Invented []string
// Internal are cited ids that ARE in the cut but declared `impact: internal`.
// Citing one is an INVENTION, not a tolerated extra: internal is the class
// that earns no changelog line at all (Impact.InChangelog), so a line about
// one tells a user that plumbing work changed their world. It is named apart
// from Invented only because the fix differs — delete the line, or correct the
// record's impact if the judgement was wrong.
Internal []string
}
IncompleteError is the completeness bijection's refusal: the composed document and the cut do not describe the same set of records, so NOTHING was written.
The three groups are kept apart because they are three different mistakes with three different fixes, and an operator handed one merged list cannot tell which they have.
func (*IncompleteError) Error ¶
func (e *IncompleteError) Error() string
Error names every id in every group, and what each group means. It is the loud-stage: an operator reading it must be able to fix the document without re-deriving the cut by hand.
type IngestResult ¶
type IngestResult struct {
// Cut is the deterministic cut the prose was validated against.
Cut Cut `json:"cut"`
// Written reports whether the release record was updated. It is false both
// for a refused cut and (with an error) for a refused document; nothing
// partial is ever written.
Written bool `json:"written"`
// Path is the record written, repo-relative; empty when nothing was written.
Path string `json:"path"`
// Heading is the dated heading written — the exact line auto-release.yml
// greps to decide what to tag.
Heading string `json:"heading"`
// Lines counts the changelog lines written.
Lines int `json:"lines"`
// Cited is the required record-id set, sorted: the bijection's proof, so a
// reviewer can check the release record against it without re-running a cut.
Cited []string `json:"cited"`
// Page reports the release page: written (and what moved to the archive), or
// why no page was written.
Page PageResult `json:"page"`
// Moved lists every targeted intent the cut passed without shipping it,
// each rewritten to `next` in the same change unless it already named
// `next` (itd-2609212103572513 criterion 3, ruling BS1 of 2026-09-29), and
// named in the section's move note.
Moved []launch.TargetMove `json:"moved_targets,omitempty"`
// Undo reverses the cut's writes. The ship verb applies it when a step after
// the ingest refuses, so a refused ship leaves nothing behind.
Undo UndoPlan `json:"-"`
}
IngestResult is the write step's transport-agnostic outcome. It carries the whole Cut so a front door can render the same report the emit step renders — a refused cut reaches here as a RESULT, not an error, and must still be shown.
func Ingest ¶
Ingest validates the host-composed payload against the deterministic cut and writes the release: the archive move, the release page, and the dated section of CHANGELOG.md, in that order, rolling back on failure (write.go).
current is the caller's view of the command surface, passed in for the reason Emit states: internal/core must not walk a cobra tree. at is the clock, passed in rather than read, so the date in a durable release heading is an input a test can pin instead of a wall-clock read buried in a writer.
The outcomes are distinguishable on purpose:
(result, nil) with Written — the release landed.
(result, nil) without Written — the CUT refuses (result.Cut.Refusals says
why). A refusal is a result to render, and
the front door maps it to exit 1.
(result, *PayloadRefusal) — the PAYLOAD is refused, with every reason:
recompose it. Nothing was written.
(result, other error) — the repository cannot take the cut (an
unreadable file, a missing anchor, an
archive collision, a failed write that was
rolled back): stop.
The whole ingest — the derivation, the reads the plan is built from, and the writes — holds the CHANGELOG's lock (withChangelogLock), so two cuts of one working tree never write from the same stale read: the second derives after the first has written, and is refused as a release in flight (iss-127).
type PageResult ¶ added in v0.10.0
type PageResult struct {
Written bool `json:"written"`
Path string `json:"path,omitempty"`
Heading string `json:"heading,omitempty"`
// Archived is the path the outgoing page moved to; empty when nothing moved.
Archived string `json:"archived,omitempty"`
Headlines int `json:"headlines"`
Listed int `json:"listed"`
Quotes int `json:"quotes"`
// Reason says why no page was written, empty when one was.
Reason string `json:"reason,omitempty"`
}
PageResult reports what the cut did with the release page.
type PayloadRefusal ¶ added in v0.10.0
type PayloadRefusal struct {
Reasons []Reason `json:"reasons"`
// contains filtered or unexported fields
}
PayloadRefusal is the refusal of a composed payload: every reason found, and NOTHING written. It is the "recompose" signal; see the file comment.
func (*PayloadRefusal) Error ¶ added in v0.10.0
func (r *PayloadRefusal) Error() string
Error names every reason on its own line. It is the human render: an operator reading it must be able to see what the composer has to fix.
func (*PayloadRefusal) Unwrap ¶ added in v0.10.0
func (r *PayloadRefusal) Unwrap() error
Unwrap exposes the changelog bijection's typed verdict when it is one of the reasons.
type PressReleasePayload ¶ added in v0.10.0
type PressReleasePayload struct {
// Headlines are the intents told as prose, one paragraph each.
Headlines []Headline `json:"headlines"`
// Listed are the ids of every other intent in the set. The binary renders
// each as its record's title, so the name list carries no composer prose.
Listed []string `json:"listed"`
// Quotes are persona quotes carried word for word from the press release of
// an intent a headline tells.
Quotes []Quote `json:"quotes"`
}
PressReleasePayload is the page half of the composed payload. It is absent (or null) exactly when the cut's press-release set is empty.
type Quote ¶ added in v0.10.0
type Quote struct {
// Record is the intent whose press release carries the quote; it must be a
// headline record.
Record string `json:"record"`
// Text is the quote as the source has it, attribution included.
Text string `json:"text"`
// Attribution is the speaker as the source names them; it must appear in
// Text, so it is carried exactly as the source has it.
Attribution string `json:"attribution"`
}
Quote is one persona quote, verified against its source before it is written.
type Reason ¶ added in v0.10.0
type Reason struct {
Code ReasonCode `json:"code"`
// At is the payload path the fault concerns ("press_release.headlines[0].text").
At string `json:"at"`
// Detail is sanitised: it may quote an id, never a whole untrusted string.
Detail string `json:"detail"`
}
Reason is one fault: what kind, where in the payload, and what exactly.
type ReasonCode ¶ added in v0.10.0
type ReasonCode string
ReasonCode names one fault in a composed payload. The string values are the wire contract the host's retry loop and commands/launch.md read, so renaming a constant is safe and changing a value is a contract change.
const ( // Decode faults: the document is unusable, so only one can be found. ReasonPayloadOversize ReasonCode = "payload-oversize" ReasonMalformedJSON ReasonCode = "malformed-json" ReasonUnknownField ReasonCode = "unknown-field" ReasonTrailingData ReasonCode = "trailing-data" ReasonSchemaVersion ReasonCode = "schema-version" ReasonPromptVersion ReasonCode = "prompt-version" // ReasonStaleCut: next_tag differs from the re-derived cut. The host re-runs // the emit step before it recomposes. ReasonStaleCut ReasonCode = "stale-cut" // Faults shared by the changelog entries and the page. ReasonTextOversize ReasonCode = "text-oversize" ReasonMalformedID ReasonCode = "malformed-id" ReasonNoCitation ReasonCode = "no-citation" ReasonEmptyProse ReasonCode = "empty-prose" // Changelog faults. ReasonNoEntries ReasonCode = "no-entries" ReasonSection ReasonCode = "section" ReasonSectionNotWritable ReasonCode = "section-not-writable" ReasonChangelogMissing ReasonCode = "changelog-missing" ReasonChangelogInvented ReasonCode = "changelog-invented" ReasonChangelogInternal ReasonCode = "changelog-internal" // Release-page faults. ReasonMissing ReasonCode = "missing" ReasonOutsideSet ReasonCode = "outside-set" ReasonDuplicateCitation ReasonCode = "duplicate-citation" ReasonNoHeadline ReasonCode = "no-headline" ReasonPageForEmptySet ReasonCode = "page-for-empty-set" ReasonHeading ReasonCode = "heading" ReasonFence ReasonCode = "fence" // ReasonBlockquote: a page text that would pose as a verified quote, by // opening with `>` or by attributing words to a persona in a headline. ReasonBlockquote ReasonCode = "blockquote" ReasonQuoteSource ReasonCode = "quote-source" ReasonQuoteNotVerbatim ReasonCode = "quote-not-verbatim" // ReasonOutboundPolicy: the rendered page or changelog section carries a // session URL or a tool attribution footer (scanner.CheckOutbound). ReasonOutboundPolicy ReasonCode = "outbound-policy" // ReasonPersonaRegistry: the rendered page attributes words to a persona the // repository's registry does not hold (record-lint's persona_registry rule, // run at the cut because the root page sits outside record-lint's roots). ReasonPersonaRegistry ReasonCode = "persona-registry" // ReasonPrivacy: the rendered page or changelog section carries a hard_fail // finding of the canonical scanner (a token, a key, the caller's own home or // identity), the bar the launch scan holds the same files to. ReasonPrivacy ReasonCode = "privacy" )
The reason codes, grouped by the stage that finds them.
type ReceiptsProtocol ¶ added in v0.11.0
type ReceiptsProtocol struct {
// Workflow is the release workflow the gate list was read from.
Workflow string `json:"workflow"`
// Armed reports that the workflow runs a receipt gate at all.
Armed bool `json:"armed"`
// RequiredGates are the semantic gates the release job requires, in the
// workflow's order. Empty when the workflow arms none.
RequiredGates []string `json:"required_gates"`
// Steps are the checklist, in order, unnumbered.
Steps []string `json:"steps"`
}
ReceiptsProtocol is the receipts protocol the emit step ends with (itd-93 AC8, iss-327): what the operator does between the cut and the merge so the release job's receipt gate admits the release. The steps are composed here, from the release workflow's own required-gate list; the front door numbers and renders them.
It exists because the protocol used to live only in the runbook, and a first release is exactly when nobody has read the runbook: a one-commit release branch reached a tag and failed there, the most expensive place to learn it.
func ReceiptsProtocolFor ¶ added in v0.11.0
func ReceiptsProtocolFor(root string) (ReceiptsProtocol, error)
ReceiptsProtocolFor composes the receipts protocol for the repository at root, reading which semantic gates to run from its committed release workflow — the same list the release job and `abcd launch receipts` read, so the checklist can never ask for a gate the release does not require, or omit one it does.
type Refusal ¶
type Refusal struct {
Kind RefusalKind `json:"kind"`
Reason string `json:"reason"`
// Records lists the blocking record ids when the refusal is about records,
// so a front door can act on the refusal without parsing its prose.
Records []string `json:"records,omitempty"`
}
Refusal is one reason a cut cannot proceed. It always names something specific — a record, a version, a command — because "refused" on its own tells an operator nothing about what to fix.
type RefusalKind ¶
type RefusalKind string
RefusalKind classifies why a cut cannot proceed. The string values are the wire format a front door emits, so renaming a constant is safe and changing a value is a contract change.
const ( // RefusalNoReleaseTag: no immutable base to measure the cut from. RefusalNoReleaseTag RefusalKind = "no-release-tag" // RefusalReleaseInFlight: the newest CHANGELOG heading is ahead of the // newest tag, so a release sits between its merge and its tag. RefusalReleaseInFlight RefusalKind = "release-in-flight" // RefusalUnlabelled: a record added by the cut carries no valid impact. RefusalUnlabelled RefusalKind = "unlabelled-record" // RefusalUncommittedRecords: a record in a terminal folder differs from // HEAD, where the cut reads it, so the cut would leave the change out. RefusalUncommittedRecords RefusalKind = "uncommitted-records" // RefusalStaleIntent: an intent in planned/ has no open spec left — every // spec realising it has closed and the record never moved. RefusalStaleIntent RefusalKind = "stale-intent" // RefusalSurfaceGuard: the surface guardrail failed or could not compare. RefusalSurfaceGuard RefusalKind = "surface-guard" // RefusalUnfixedFinding: a consequential finding this cycle captured is // still open, with no recorded decision to defer it. RefusalUnfixedFinding RefusalKind = "unfixed-finding" // RefusalDeletedFinding: a consequential record the anchor held in open/ has // been removed from the ledger rather than answered. It is a kind of its own // rather than a shape of unfixed-finding because the remedy differs — the // record has to come back before it can be resolved, waived or wontfixed — // and because a front door acting on the refusal cannot open a file that is // no longer there. RefusalDeletedFinding RefusalKind = "deleted-finding" // RefusalEmptyCut: nothing user-facing shipped, so there is no release. RefusalEmptyCut RefusalKind = "empty-cut" // RefusalDocFidelity: the brief lags the binary being cut (itd-60) — a // shipped surface no chapter names, whatever the cut ships, or, where an // intent shipped since the tag, no saved docs review names the commit // being cut or the review confirms a false sentence. RefusalDocFidelity RefusalKind = "doc-fidelity" )
The refusal kinds. Every one of them is fail-closed: the cut stops rather than deriving a number or a changelog that would be wrong.
type Section ¶
type Section string
Section is a Keep-a-Changelog section name — the agent's editorial judgement about a record, which is why it is a payload field and not derived from impact.
const ( SectionAdded Section = "Added" SectionChanged Section = "Changed" SectionDeprecated Section = "Deprecated" SectionRemoved Section = "Removed" SectionFixed Section = "Fixed" SectionSecurity Section = "Security" )
The six registered Keep-a-Changelog 1.1.0 sections. The set is CLOSED: an unregistered section is a structural refusal, not a new heading, because the release record's shape is not the composer's to extend.
type UndoPlan ¶ added in v0.10.0
type UndoPlan struct {
// contains filtered or unexported fields
}
UndoPlan is what it takes to put the tree back as it was before a cut wrote. The ship verb applies it when a step AFTER the ingest refuses (the payload render), so a refused ship leaves no release record behind.
func (UndoPlan) Apply ¶ added in v0.10.0
Apply undoes every write the cut made, in reverse, and returns a description of each undo that failed (empty when the tree is restored).
It holds the CHANGELOG's lock, as the cut's writes did, so the restore is not interleaved with another cut's read of the files it puts back.