release

package
v0.6.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
const ChangelogSchemaVersion = 1

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.

View Source
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.

Variables

This section is empty.

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"`
}

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"`
	// 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

func Emit(root string, current surface.Snapshot) (Cut, error)

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 three refusals (no tag, release in flight, an unlabelled record) 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"`
}

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 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"`
}

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

func Ingest(root string, current surface.Snapshot, raw []byte, at time.Time) (IngestResult, error)

Ingest validates the host-composed changelog against the deterministic cut and writes the dated section into CHANGELOG.md.

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 three outcomes are distinguishable on purpose:

(result, nil) with Written        — the section 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, error)                   — the DOCUMENT is unusable (a payload fault
                                    or a failed bijection). Exit 2; the file
                                    is byte-identical to what it was.

Every refusal path returns before the single atomic write, so a half-composed changelog can never land.

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"
	// RefusalStaleIntent: an intent in planned/ has a spec that has closed.
	RefusalStaleIntent RefusalKind = "stale-intent"
	// RefusalSurfaceGuard: the surface guardrail failed or could not compare.
	RefusalSurfaceGuard RefusalKind = "surface-guard"
	// RefusalEmptyCut: nothing user-facing shipped, so there is no release.
	RefusalEmptyCut RefusalKind = "empty-cut"
)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL