launch

package
v0.13.3 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package launch is abcd's transport-agnostic launch engine: it assembles the release bundle under a default-deny taxonomy, runs the native secret+PII scan, checks manifest lockstep, and previews newest-per-line retention — all as a dry-run that renders decisions without writing an artefact or touching the network. It performs no printing and no os.Exit, so it is fully testable and reusable across surfaces.

The load-bearing invariant (adr-18/adr-28): the .abcd/** namespace and every other denied namespace can NEVER enter the bundle. This is a STRUCTURAL deny, not an allowlist toggle — no include pattern can promote a denied path.

Index

Constants

View Source
const (
	// TierHardFail — a finding refuses the release.
	TierHardFail = "hard-fail"
	// TierWarn — a finding is surfaced and refuses nothing, unless the
	// repository configures the suite strict.
	TierWarn = "warn"
)

Gate tiers: what a finding in the row does to the release.

View Source
const (
	MarkerBlockBegin = "<!-- BEGIN ABCD -->"
	MarkerBlockEnd   = "<!-- END ABCD -->"
)

The managed marker pair abcd writes around its block in an agent instructions file. The spelling is the one internal/core/ahoy installs and strips; ahoy imports this package, so the pair is declared here and a test proves ahoy recognises exactly this spelling (gates_marker_drift_test.go).

View Source
const (
	ModePreview = "preview"
	ModeCut     = "cut"
)

Report modes: which run wrote the report.

View Source
const (
	VerdictClear   = "clear"
	VerdictRefused = "refused"
)

Report verdicts.

View Source
const ArchiveTreeDescription = "the tree the release tag would archive (git archive's view of HEAD, export-ignore honoured), minus the record namespace"

ArchiveTreeDescription names the tree a non-plugin kind's preview scans, for the report line that says which tree was scanned.

View Source
const ArtefactRelPath = ".abcd/config/artefact.json"

ArtefactRelPath is the declaration's home, repo-relative.

View Source
const BinaryNamePattern = `abcd(-[a-z0-9]+-[a-z0-9]+)?(\.exe)?`

BinaryNamePattern is the ONE spelling of a built abcd binary's file name — the `abcd-<goos>-<goarch>` names `make build` and the release workflow publish, plus the `.exe` spelling, AND the bare `abcd` that `go build ./cmd/abcd` produces. It is unanchored so a recogniser can embed it; every reader that asks "is this file an abcd binary?" builds on it (BinaryNameRe here, and ahoy's recognisers of an abcd command in the harness settings), so a new build name is taught in one place.

The bare name matters most of the three: it is the name the bootstrap's own refusal text tells a user to build, and the name the binary runs under inside the plugin root, so a control that covered only the cross-compiled spellings had its hole at the most likely file.

View Source
const PayloadTreeDescription = "the plugin payload (the include set in .abcd/config/launch-payload.json)"

PayloadTreeDescription names the tree a plugin's preview scans.

View Source
const TargetNext = "next"

TargetNext is the symbolic target: the next release, whatever version it derives. A derived release cannot be numbered before it is cut, so `next` is the one spelling that names it without guessing.

View Source
const TargetReleaseKey = "target_release"

TargetReleaseKey is the intent frontmatter key the target lives under, spelled once.

Variables

ArtefactKinds is the accepted set, in the order a refusal names it.

View Source
var BinaryNameRe = regexp.MustCompile(`^` + BinaryNamePattern + `$`)

BinaryNameRe matches the WHOLE basename of a built abcd binary: a document about an artefact (`docs/abcd-darwin-arm64.md`) is prose, not a binary, and denying by substring would take it too.

View Source
var DenyNamespaces = map[string]struct{}{
	".git": {}, ".abcd": {}, ".flow": {}, ".work": {}, ".specstory": {}, "memory": {},
}

DenyNamespaces are namespace names that never ship. The structural deny (adr-18) binds EVERY path component case-insensitively (see pathContainsDeniedSegment), not just the first segment: a denied name nested under an included tree, or spelled in a different case, is denied all the same (GHSA-g2v7-wfmv-v28r, #335). NOT overridable by any allowlist. Mirrors launch_resolve DENY_NAMESPACES.

View Source
var ErrArchivePinMismatch = errors.New("the committed marketplace pin does not match the rendered plugin archive")

ErrArchivePinMismatch reports that the committed catalog does not name the archive a render just produced — no pin at all, a pin for another release, or a digest the render does not reproduce. It is the release gate's refusal.

View Source
var ErrArchiveRepositoryMismatch = errors.New("the plugin archive's address is not the releasing repository's release")

ErrArchiveRepositoryMismatch reports that the archive's download address does not sit under the releasing repository's download path for the tag. The address derives from plugin.json's repository, which a rename, a transfer or a fork leaves naming another repository: the pin still matches the render, and every install 404s. It is the release gate's refusal, like ErrArchivePinMismatch.

View Source
var ErrDirtyTree = errors.New("the working tree is not clean")

ErrDirtyTree reports that the working tree carries uncommitted changes (or that its state could not be read) and the cut was not told to allow them.

View Source
var ErrNoArtefact = errors.New("this repository declares no artefact kind")

ErrNoArtefact reports a repository that has not declared its artefact kind. It is carried inside a PreflightError whose message names the declaration's home and the accepted kinds, so a caller that recognises it can say more and a caller that does not still relays an actionable refusal.

View Source
var ErrNoLaunchPayload = errors.New("this repository declares no launch payload")

ErrNoLaunchPayload reports that the repository declares no launch payload at all: it carries no include config. That is not a misconfiguration — a repository that ships no plugin bundle legitimately has none — so the front door recognises it and names the release path such a repository does have, rather than relaying a missing-file error (iss-2608270559313719).

View Source
var ErrNotAPlugin = errors.New("the repository does not declare a plugin")

ErrNotAPlugin reports a plugin-only operation — staging or archiving a plugin payload — asked of a repository that declares another artefact kind.

View Source
var ErrParityBaselineUnreadable = errors.New("the payload parity baseline could not be read")

ErrParityBaselineUnreadable is the precheck's refusal when the baseline the diff is measured against cannot be read.

View Source
var ErrPayloadDrift = errors.New("the rendered payload failed the public lockstep check")

ErrPayloadDrift reports that the rendered payload failed the public lockstep check. It is its own error because it means the render itself is wrong (a pinned location was missed), not that the caller's input was.

View Source
var ErrPayloadGateRefused = errors.New("the payload failed a launch pre-flight gate")

ErrPayloadGateRefused reports that the payload failed one of the suite's content gates (marker-block sanity, change narration), or a warn-tier gate in a repository that configured the suite strict.

View Source
var ErrPayloadPageUnloadable = fmt.Errorf("%w (deep tier)", ErrPayloadUninstallable)

ErrPayloadPageUnloadable is the precheck's refusal when a declared page would not load. It wraps ErrPayloadUninstallable, so a caller matching the installability refusal matches this one too.

View Source
var ErrPayloadScanRefused = errors.New("the payload failed the secret/PII scan gate")

ErrPayloadScanRefused reports that the payload the render would materialise carries a secret/PII hard-fail, an unscannable coverage gap, or was scanned by an unavailable scanner — the fail-closed secret/PII gate the materialising path must pass before any write (gh-328). It is the render's twin of the ship gate's scan refusal: DryRun runs the scan advisory-only and Ship (the gated path) has no production caller, so the live `launch ship --payload-dir` render was the sole materialising door that never invoked the scanner.

View Source
var ErrPayloadUninstallable = errors.New("the rendered payload failed the installability smoke")

ErrPayloadUninstallable reports that the rendered payload declares a surface it does not carry. It is distinct from ErrPayloadDrift because the fault is the payload's CONTENTS, not the version stamp: the manifests agree perfectly and the plugin still would not install.

View Source
var ErrShipBlocked = errors.New("ship blocked by a launch gate")

ErrShipBlocked is returned when any gate hard-fails.

Functions

func ArchiveReleaseURL added in v0.10.0

func ArchiveReleaseURL(repoRoot, version string) (string, error)

ArchiveReleaseURL is the address the release workflow publishes version's archive at: <repository>/releases/download/v<version>/<archive name>.

func BumpTier

func BumpTier(prev, next Semver) string

BumpTier names the SemVer component that moved from prev to next: "major", "minor", "patch", or "" when the two cores are equal.

It reads the actual version delta rather than mapping an impact, because the mapping is not fixed: pre-1.0, changelog.DeriveNext turns a breaking impact into a MINOR bump and an additive one into a PATCH. A tier derived from the impact would therefore mislabel every pre-1.0 release in the published manifest, while the delta is true at any point on the 0.x/1.x boundary.

func CheckArchiveRepository added in v0.10.0

func CheckArchiveRepository(url, repository, tag string) error

CheckArchiveRepository refuses, with ErrArchiveRepositoryMismatch, unless url sits under https://github.com/<repository>/releases/download/<tag>/ — the path the release workflow running in repository uploads tag's assets to. GitHub resolves owner and repository names case-insensitively, so the comparison is too. A repository that is not owner/name is a structural fault, not a mismatch.

func CoreGreater

func CoreGreater(a, b Semver) bool

CoreGreater reports whether a is strictly newer than b by core version. It is exported because release ordering is decided in more than one place — the retention plan here and the tag anchor the changelog derivation resolves — and a second hand-rolled field comparison would be a second, silently divergent definition of "newer".

func DeclaresPluginArchive added in v0.10.0

func DeclaresPluginArchive(repoRoot string) (bool, error)

DeclaresPluginArchive reports whether the repository's release publishes the pinned plugin archive: its version-location contract carries `"publishes_plugin_archive": true`. Nothing else counts. The contract alone says where the version lives, not that a release uploads an archive — a managed repository scaffolds workflows that upload none, and a catalog pinned there names an asset nothing publishes. An absent contract or key is false; a key that is not a boolean is refused rather than read as either answer.

func DirtyPayloadFiles added in v0.10.0

func DirtyPayloadFiles(repoRoot string, bundle Bundle) ([]string, error)

DirtyPayloadFiles names the payload files whose working-tree bytes are not the committed ones: modified or staged tracked files, and untracked files the payload would carry.

The ship renders the archive from the WORKING TREE to learn the digest it commits, while the release gate renders it from the tagged COMMIT. A payload file that differs between the two makes the pin unreproducible, and the release would then refuse at a point where the version is already tagged. So the ship refuses such a tree before it writes anything.

It reads the tree through DirtyTreeFiles, the dirty-tree gate's own reader, and keeps the payload's share: unlike that gate, this refusal has no --allow-dirty, because an unreproducible pin is wrong whoever allows it.

func DirtyTreeFiles added in v0.11.0

func DirtyTreeFiles(repoRoot string) ([]string, error)

DirtyTreeFiles lists the working tree's uncommitted paths, repo-relative and sorted: every tracked change against HEAD, staged or not, and every untracked file git does not ignore. The local tier is never listed. A tree whose state git cannot read (no repository, no HEAD) is an error — never an empty list, which would read as clean.

func IsBinaryName added in v0.13.1

func IsBinaryName(base string) bool

IsBinaryName reports whether base is a built abcd binary's file name.

func IsStrictSemver

func IsStrictSemver(value string) bool

IsStrictSemver reports whether value is a strict SemVer 2.0.0 string.

func LoadIncludes

func LoadIncludes(repoRoot string) ([]string, error)

LoadIncludes reads .abcd/config/launch-payload.json, hand-validates its shape (no schema dependency — see §1.1), and returns the normalised, de-duplicated include patterns. A missing/malformed config, or an absolute / ".." / denied-rooted include, is a PreflightError.

func MarshalArtefact added in v0.11.1

func MarshalArtefact(art Artefact) []byte

MarshalArtefact renders a declaration the way ahoy writes it: indented, with an empty lockstep list and the site opt-in spelled out, so the file shows the keys it admits.

func PluginArchiveName added in v0.10.0

func PluginArchiveName(plugin, version string) string

PluginArchiveName is the release asset name for one plugin at one version.

func PrecheckPluginArchive added in v0.10.0

func PrecheckPluginArchive(repoRoot string) error

PrecheckPluginArchive makes the version-free refusals of the archive half of a ship: the plugin manifest names a plugin and a repository the release download address can be derived from. It performs zero writes.

func ReleaseRepository added in v0.11.0

func ReleaseRepository(repoRoot string) (string, error)

ReleaseRepository is the https://github.com/<owner>/<repo> address the working tree's plugin manifest names: where the plugin's releases, and their assets, are published.

func RenderPluginArchive added in v0.10.0

func RenderPluginArchive(req PayloadRenderRequest, outDir string) (PluginArchive, PayloadRenderResult, error)

RenderPluginArchive renders the release payload through RenderPayload into req.Dest (a staging directory under the same rules as any render: outside the repository, empty or absent) and packs it into outDir as the release archive. It returns the archive and the render it was packed from.

outDir must already exist; the archive is written there and nowhere else, and an archive of the same name already in outDir is refused rather than replaced.

func ShallowCheckout added in v0.11.0

func ShallowCheckout(repoRoot string) (bool, error)

ShallowCheckout reports whether the checkout at repoRoot is a shallow clone. A shallow clone's tag listing succeeds while holding only the tags that were fetched, so neither the retention plan nor the parity diff's baseline may read that listing as the release set. An error is a checkout whose shallowness git could not report, which a caller treats as shallow.

func ValidArtefactKind added in v0.11.1

func ValidArtefactKind(k string) bool

ValidArtefactKind reports whether k is one of the accepted kinds.

func ValidTargetRelease added in v0.12.0

func ValidTargetRelease(value string) error

ValidTargetRelease reports whether value is a legal target: `next`, or a release tag `vX.Y.Z` (the strict SemVer core, with a leading `v`, no pre-release and no build metadata, because a release this repository cuts is always a bare core version). The error names the accepted shapes.

func ValidateBaselineTag added in v0.11.0

func ValidateBaselineTag(repoRoot, tag string) error

ValidateBaselineTag refuses a configured baseline that is not a strict v-prefixed release tag present in this checkout. It is the check a front door makes on an operator-named baseline before anything runs, so a wrong one errors rather than reading as a first launch.

func ValidateGitHubRepository added in v0.10.0

func ValidateGitHubRepository(repository string) error

ValidateGitHubRepository refuses a repository that is not a bare GitHub owner/name, the shape a workflow's GITHUB_REPOSITORY carries.

func VerifyArchivePin added in v0.10.0

func VerifyArchivePin(repoRoot string, a PluginArchive) error

VerifyArchivePin proves the committed catalog names exactly this archive: the address the release publishes it at, and its digest. Any other state is ErrArchivePinMismatch, with both sides named.

func WriteArchivePin added in v0.10.0

func WriteArchivePin(repoRoot string, pin ArchivePin) error

WriteArchivePin rewrites the plugin's catalog listing so its source is the pinned archive. Only the source changes; every other key of the listing and of the catalog is kept.

func WritePreflightReport added in v0.11.0

func WritePreflightReport(repoRoot string, rep PreflightReport) (string, error)

WritePreflightReport writes rep as preflight.json and preflight.md into a directory of its own under .abcd/.work.local/logs/launch/, named for the report's instant, and returns that directory repo-relative. Two runs in one second get two directories: the directory is created exclusively, never reused.

repoRoot is resolved through its symlinks before the directory proof: the launch verbs hand in the shell's logical working directory, and a checkout entered through a symlinked path is the user's own, so only a symlink at or below the checkout's .abcd is refused (the iss-2609261108448674 sweep).

Types

type ArchivePin added in v0.10.0

type ArchivePin struct {
	URL    string `json:"url"`
	SHA256 string `json:"sha256"`
}

ArchivePin is the marketplace source that names a release archive.

func ReadArchivePin added in v0.10.0

func ReadArchivePin(repoRoot string) (ArchivePin, bool, error)

ReadArchivePin returns the archive source the working tree's catalog names for the plugin. ok is false, with no error, when the plugin's listing carries any other source (the relative-path "./" included).

type Artefact added in v0.11.1

type Artefact struct {
	Kind ArtefactKind `json:"kind"`
	// Lockstep is the declared secondaries a non-plugin kind holds in lockstep
	// with its primary (decision 2). A plugin keeps the pinned manifest table.
	Lockstep []LockstepFile `json:"lockstep,omitempty"`
	// Site is the release-rendered site opt-in (decision 9). It is read and
	// validated here and acted on by itd-2609061543533170, not by this intent.
	Site bool `json:"site,omitempty"`
}

Artefact is a validated declaration.

func LoadArtefact added in v0.11.1

func LoadArtefact(repoRoot string) (Artefact, error)

LoadArtefact reads and validates the repository's artefact declaration. An absent file is a PreflightError wrapping ErrNoArtefact; every other fault — an unreadable or malformed file, an unknown kind, a lockstep path that is not a contained repo-relative path — is a PreflightError naming what is wrong. Whether a declared lockstep file can be READ is the lockstep check's to say, where the refusal names the path.

func LoadArtefactOrPlugin added in v0.11.1

func LoadArtefactOrPlugin(repoRoot string) (Artefact, error)

LoadArtefactOrPlugin reads the declaration for the verbs that predate it and stay lenient about its absence: an absent file is the plugin shape they have always assumed, while a present file is held to the one reader like anywhere else, so an unknown kind refuses every verb.

func ParseArtefact added in v0.11.1

func ParseArtefact(data []byte) (Artefact, error)

ParseArtefact validates a declaration's bytes. It is exported for the writer in ahoy, which proves what it is about to write reads back.

func (Artefact) IsPlugin added in v0.11.1

func (a Artefact) IsPlugin() bool

IsPlugin reports whether the declared kind is the plugin shape.

type ArtefactKind added in v0.11.1

type ArtefactKind string

ArtefactKind is what a repository ships.

const (
	// KindPlugin is the shipped shape: a harness plugin whose payload, manifests
	// and marketplace listing the launch gates already judge.
	KindPlugin ArtefactKind = "plugin"
	// KindBinary is a built program, a Go binary in the first cut.
	KindBinary ArtefactKind = "binary"
	// KindApplication is an application with its own build and publish steps.
	KindApplication ArtefactKind = "application"
)

The kinds the first cut accepts. Any other value is refused by name.

type Bundle

type Bundle struct {
	Included []IncludedFile `json:"files"`
	Excluded []ExcludedFile `json:"excluded"`
	Rejected []RejectedFile `json:"rejected"`
	Warnings []string       `json:"warnings"`
}

Bundle is the classified resolution outcome.

func ResolveArchiveBundle added in v0.11.1

func ResolveArchiveBundle(repoRoot string) (Bundle, error)

ResolveArchiveBundle is the bundle of a non-plugin artefact kind that declares no payload include config (itd-2609150819432059, decision 8): the files an archive of HEAD would carry, classified under the same structural deny a plugin payload is held to. A path with a denied segment is excluded(denied_namespace), exactly as the plugin resolver excludes it; a link is excluded(symlink); a control character in a path is rejected, as it is in a plugin payload. Every other file is included, read from the working tree the way a plugin payload's files are, so an uncommitted edit is what the dirty-tree gate reports rather than something this listing hides.

func ResolveBundle

func ResolveBundle(repoRoot string, includes []string) (Bundle, error)

ResolveBundle walks repoRoot, matches candidates against includes, and classifies each into Included / Excluded / Rejected under the ordered algorithm. includes==nil loads the committed config via LoadIncludes (a preflight fault is returned as an error).

func (Bundle) HasViolation

func (b Bundle) HasViolation() bool

HasViolation reports whether any rejected[] entry exists. ship hard-fails on true; dry-run reports it but still exits 0.

type ChangelogEntry

type ChangelogEntry struct {
	// Tier is the SemVer component the cut moved: patch, minor or major.
	Tier string
	// Reason is the human sentence explaining the bump, e.g. "additive itd-67 shipped".
	Reason string
	// Date is the release date; only its calendar day is recorded.
	Date time.Time
	// SourceSHA is the commit the release was cut from.
	SourceSHA string
}

ChangelogEntry is the marketplace plugin entry's per-release record (adr-20 R3), minus its version.

The version is deliberately NOT a field: the render writes the one derived version into this entry and into both version pointers from a single input, so the entry cannot disagree with the manifest it travels in. Everything here is supplied by the caller — a core that read the clock or the git HEAD itself would put two unpinnable inputs inside a durable release artefact.

type CitationPreflight

type CitationPreflight struct {
	// Unreadable, when non-empty, means the measurement could not be taken at
	// all — a docs-lint config that arms the rule but does not parse, or a
	// baseline the loader refuses. It is a separate state from "not armed"
	// because reporting a broken gate as an absent one is a false statement
	// about a real requirement, and it refuses.
	Unreadable string
	// Present reports whether a baseline exists at all.
	Present bool
	// Cited, Recorded and Missing describe coverage.
	Cited    int
	Recorded int
	Missing  int
	// Broken counts citations recorded as dead.
	Broken int
	// Approaching counts entries inside the nag window before the blocking
	// threshold; Overdue counts those past it.
	Approaching int
	Overdue     int
	// ApproachingURLs and OverdueURLs name them, so a nag and a refusal tell an
	// operator WHICH links to deal with rather than only how many.
	ApproachingURLs []string
	OverdueURLs     []string
}

CitationPreflight is the citation baseline's state, measured by the caller. A nil pointer means the repo has not armed the citation gate, and the gate reports itself as not run rather than silently passing.

type ClosureFn

type ClosureFn func(repoRoot string) (map[string]struct{}, error)

ClosureFn returns the runtime-closure set (repo-relative POSIX paths) for the scripts/ include — the AST-reachable set the shipped plugin ships, never the whole dev tree. Since the Go payload layout is not yet settled, the default reads a pinned closure list from config rather than re-deriving via Python AST (spec §1 step 10 — the single open dependency). A nil map means no closure scoping is applied (scripts/ then behaves like any other include).

type DeepSmokeReport added in v0.11.0

type DeepSmokeReport struct {
	Tier SmokeTier `json:"tier"`
	OK   bool      `json:"ok"`
	// Checked counts the pages asked about, so a pass over a payload with no
	// pages is visibly vacuous.
	Checked  int            `json:"checked"`
	Pages    []PageHelp     `json:"pages,omitempty"`
	Findings []SmokeFinding `json:"findings,omitempty"`
}

DeepSmokeReport is one run of the deep tier.

func SmokeDeep added in v0.11.0

func SmokeDeep(root string, run PageRunner) DeepSmokeReport

SmokeDeep runs the deep tier over a materialised payload at root, resolving the SAME declared surface the light tier asserts over and handing every page to run. Like SmokeLight it never returns an error: a runner that fails is the most serious finding it can make.

type DirtyPolicy added in v0.11.0

type DirtyPolicy int

DirtyPolicy is how the dirty-tree gate treats uncommitted changes.

const (
	// DirtyRefuse is the zero value, so a caller that says nothing fails
	// closed: an uncommitted change, or a tree whose state cannot be read,
	// refuses.
	DirtyRefuse DirtyPolicy = iota
	// DirtyAllow is --allow-dirty: the uncommitted changes are carried, and the
	// pre-flight report records the override and every path it carried. A tree
	// whose state cannot be read still refuses — the override allows dirt, not
	// blindness.
	DirtyAllow
	// DirtySkip leaves the gate out. It exists for the render a cut runs AFTER
	// its own writes (the dated CHANGELOG heading, the release page, the
	// archive pin): those writes are the cut's expected output, not dirt, and
	// the gate already ran before any of them, at the cut's start. The ordering
	// is the point — the cut's own staged changes must never read as dirt. A
	// render caller states it explicitly (PayloadRenderRequest.Dirty); nothing
	// defaults to it.
	DirtySkip
)

type DocAuditPreflight added in v0.11.0

type DocAuditPreflight struct {
	// Findings are the docs-lint findings over the configured doc roots.
	Findings []GateFinding `json:"findings,omitempty"`
	// Unreadable says why the audit could not be measured at all.
	Unreadable string `json:"unreadable,omitempty"`
	// NotMeasured says why the caller did not measure the audit on this
	// path. The row then reports "not_measured" and makes no claim about the
	// repository's configuration.
	NotMeasured string `json:"not_measured,omitempty"`
}

DocAuditPreflight is the documentation audit's result, MEASURED by the caller: it needs the docs-lint engine in internal/core/lint, which imports this package, so the front door that holds both hands the result in as data (the CitationPreflight shape). Nil means the repository has not armed a docs-lint configuration.

type DryRunReport

type DryRunReport struct {
	Version string `json:"version"`
	// Kind is the declared artefact kind the preview ran against.
	Kind ArtefactKind `json:"kind"`
	// ScannedTree names the tree the bundle and its scan cover: the plugin
	// payload, or the tree the release tag would archive.
	ScannedTree string             `json:"scanned_tree"`
	Bundle      Bundle             `json:"bundle"`
	Scan        scanner.ScanResult `json:"scan"`
	Lockstep    LockstepResult     `json:"lockstep"`
	Retention   RetentionPlan      `json:"retention"`
	Smoke       SmokeReport        `json:"smoke"`
	// DeepSmoke is the deep installability tier, present when it was asked for.
	DeepSmoke *DeepSmokeReport `json:"deep_smoke,omitempty"`
	// Parity is the file-level diff against the previous release's payload.
	Parity        *ParityReport `json:"parity,omitempty"`
	Gates         []GateSummary `json:"gates"`
	WouldPublish  bool          `json:"would_publish"` // always false in dry-run
	WouldRefuseOn []string      `json:"would_refuse_on,omitempty"`
	// Warnings are the warn-tier concerns: surfaced, refusing nothing unless
	// the repository configures the suite strict.
	Warnings []string `json:"warnings,omitempty"`
	// ReportPath is the repo-relative directory the front door wrote this
	// preview's pre-flight report into; ReportError says why it could not.
	ReportPath  string `json:"report_path,omitempty"`
	ReportError string `json:"report_error,omitempty"`
	// Targets lists every planned intent that names a release it must land by
	// and has not shipped (itd-2609212103572513): reported, never refused on.
	Targets []TargetedIntent `json:"targets,omitempty"`
}

DryRunReport is the full dry-run preview. No artefact is written.

func DryRun

func DryRun(req DryRunRequest) (DryRunReport, error)

DryRun assembles the bundle, scans it, checks lockstep, previews retention and runs the pre-flight gate suite, then reports what a real ship WOULD refuse on. It ALWAYS returns exit-0 semantics: an error is returned only for a preflight fault (bad include config) that makes a report impossible — never on a finding. It writes nothing; the front door writes the pre-flight report.

func (DryRunReport) PreflightReport added in v0.11.0

func (r DryRunReport) PreflightReport(at time.Time) PreflightReport

PreflightReport is the preview's pre-flight record. The clock is the caller's: a report is a durable artefact, and a core that read the clock itself would put an unpinnable input inside it.

type DryRunRequest

type DryRunRequest struct {
	RepoRoot string
	// Version is the release version this launch would publish, SUPPLIED by the
	// caller. adr-19 leaves no version key in the source tree, so there is
	// nothing here for the core to read: the version is a fact about the release
	// cut, and the front door that knows the cut injects it. Empty is honest —
	// it means the caller could not name one, and retention says so.
	Version      string
	ExistingTags []Semver // injected; nil → default `git tag -l v*` provider
	// Citations is the citation baseline's state, MEASURED by the caller.
	// Grading it needs internal/core/lint, which imports this package for its
	// semver — so the front door that holds both hands the result in as data
	// rather than this package reaching back. Nil means the repo has not armed
	// the citation gate.
	Citations *CitationPreflight
	// Receipts is the semantic-pass receipts' state, MEASURED by the caller for
	// the same reason as Citations: it needs a git rev-parse and a directory
	// read, and this package does not reach back through the front door. Nil
	// means the measurement was not taken, which the gate reports as such rather
	// than as an absence of receipts.
	Receipts *ReceiptPreflight
	// DocAudit is the documentation audit's result, MEASURED by the caller for
	// the same reason as Citations. Nil means the repository has not armed a
	// docs-lint configuration, which the gate reports as such.
	DocAudit *DocAuditPreflight
	// Parity is the parity diff's input: the previous release's tag and, when
	// the operator asked for it, the release-asset fetcher. Nil runs no diff.
	Parity *ParityInput
	// DeepSmoke is the isolated page runner the installability smoke's deep
	// tier renders every page through. Nil keeps the preview at the light tier:
	// the deep tier is opt-in here and always on in the cut.
	DeepSmoke PageRunner
	// Targets are the planned intents that name a release they must land by
	// (itd-2609212103572513), MEASURED by the caller for the reason Citations
	// is: reading them needs the intent store, which sits above this package.
	// The preview lists them and refuses nothing on them.
	Targets []TargetedIntent
}

DryRunRequest is the input to a dry-run.

type ExcludedFile

type ExcludedFile struct {
	LogicalPath string         `json:"logical_path"`
	Reason      ExcludedReason `json:"reason"`
}

ExcludedFile is a benign exclusion.

type ExcludedReason

type ExcludedReason string

ExcludedReason is why a candidate was benignly excluded (never fails a ship).

const (
	ExcludedGitignored      ExcludedReason = "gitignored"
	ExcludedUnmatchedGlob   ExcludedReason = "unmatched_glob"
	ExcludedDeniedNamespace ExcludedReason = "denied_namespace"
)
const ExcludedSymlink ExcludedReason = "symlink"

ExcludedSymlink is a link in the archived tree. An archive carries a link as the path it names, not as content, so there is nothing of it to scan, and reading through it would scan whatever the working tree's target is instead.

type GateFinding added in v0.11.0

type GateFinding struct {
	File   string `json:"file,omitempty"`
	Line   int    `json:"line,omitempty"`
	Detail string `json:"detail"`
}

GateFinding is one concern a gate raised, located where it can be fixed.

func (GateFinding) String added in v0.11.0

func (f GateFinding) String() string

String renders the finding as file:line: detail, dropping the parts it lacks.

type GatePolicy added in v0.11.0

type GatePolicy struct {
	// StrictWarnings makes a warn-tier finding refuse the release.
	StrictWarnings bool `json:"strict_warnings"`
}

GatePolicy is the repository's configuration of the suite, read from the launch-payload config beside the include list.

func LoadGatePolicy added in v0.11.0

func LoadGatePolicy(repoRoot string) (GatePolicy, error)

LoadGatePolicy reads the suite's configuration from the launch-payload config. An absent key is the default (warnings do not block); a key that is present and not a boolean is a PreflightError, like any other malformed launch config.

type GateSummary

type GateSummary struct {
	Name string `json:"name"`
	// Status is "ran", "not_implemented", "not_armed" (the repository has not
	// adopted what the gate reads) or "host-run".
	Status string `json:"status"`
	Detail string `json:"detail"`
	// Tier is what a finding in the row does: TierHardFail refuses, TierWarn
	// surfaces. Empty for a row whose refusals are reported elsewhere.
	Tier string `json:"tier,omitempty"`
	// Findings are the row's located concerns, each also carried as a line in
	// would_refuse_on (hard-fail) or warnings (warn).
	Findings []GateFinding `json:"findings,omitempty"`
}

GateSummary records one gate's disposition.

type IncludedFile

type IncludedFile struct {
	LogicalPath         string `json:"logical_path"`
	ResolvedPath        string `json:"-"`
	DisplayResolvedPath string `json:"resolved_path"`
	GitMode             string `json:"git_mode"` // "100644" | "100755"
}

IncludedFile is a resolved payload file. Paths are repo-relative POSIX; ResolvedPath is the absolute on-disk (dereferenced) path every reader opens the file through, and it never reaches machine output (iss-81): DisplayResolvedPath is the same file named relative to the repository, which is what a report carries as resolved_path (iss-2609261954288630).

type InstallSurface

type InstallSurface struct {
	PluginName  string             `json:"plugin_name"`
	Marketplace []MarketplaceEntry `json:"marketplace"`
	Entries     []SurfaceEntry     `json:"entries"`
}

InstallSurface is everything a payload declares about what installing it would register.

func ResolveInstallSurface

func ResolveInstallSurface(tree PayloadTree) (InstallSurface, error)

ResolveInstallSurface returns everything tree declares: the plugin's name, the marketplace listings with their sources resolved, and the union of the convention and manifest surface entries.

It returns an error only when a manifest or a hooks config is PRESENT and cannot be read or parsed — a payload whose declarations cannot even be enumerated. Everything else, including a declaration pointing at nothing, is DATA: it becomes an entry, and judging it is the assertion tier's job, not resolution's.

An ABSENT manifest is an absent declaration, not a broken payload (iss-2609100506255436). The constant above fixes WHERE a plugin manifest lives, because the harness discovers it at one location only; it says nothing about WHETHER this artefact has one, and the two are separate facts. adr-19 is the precedent for the distinction inside this same file: the neighbouring release-shaped fact — where the VERSION lives — was made a per-repo declared contract (version-location.json) instead of an assumption. So a payload whose artefact is a binary, an application bundle or a library resolves to a surface that carries no plugin name and no marketplace listing, rather than failing resolution before anything else runs.

type LockstepFile added in v0.11.1

type LockstepFile struct {
	Path    string `json:"path"`
	Pointer string `json:"json_pointer,omitempty"`
}

LockstepFile is one file held in lockstep with the version-location primary. Pointer is the RFC-6901 pointer to the version inside it; empty means the primary's own pointer.

type LockstepResult

type LockstepResult struct {
	Tree       LockstepTree `json:"tree"`
	OK         bool         `json:"ok"`
	Drifts     []string     `json:"drifts,omitempty"`
	Unreadable bool         `json:"unreadable,omitempty"`
	Detail     string       `json:"detail,omitempty"`
	ExitCode   int          `json:"exit_code"` // 0 ok, 1 drift, 2 unreadable
}

LockstepResult is the outcome of a manifest lockstep check.

func CheckDeclaredLockstep added in v0.11.1

func CheckDeclaredLockstep(tree LockstepTree, repoRoot, versionLocationPath string, files []LockstepFile) LockstepResult

CheckDeclaredLockstep is the lockstep check for a non-plugin artefact kind (itd-2609150819432059, decision 2): the primary is read from version-location.json exactly as CheckLockstep reads it, and the pinned plugin-manifest table is replaced by the files the artefact declaration names. No plugin manifest is read.

Each declared file is a JSON document carrying the version at its own pointer, or at the primary's when it names none. The polarities are CheckLockstep's: DEV requires every key ABSENT (adr-19), PUBLIC requires the primary present as strict SemVer and every secondary to agree with it. A declared file that cannot be read or parsed is unreadable (exit 2) and the detail names it.

A kind that declares no lockstep list and carries no version-location contract holds nothing in lockstep, and the result is an OK that says so. A declared list with no contract to read the primary from is unreadable: there is nothing for the list to agree with.

func CheckLockstep

func CheckLockstep(tree LockstepTree, repoRoot, versionLocationPath string) LockstepResult

CheckLockstep proves the two manifests plus the version-location contract describe one release consistently.

PUBLIC: the primary version (manifest_path + json_pointer from version-location.json) must be a present, non-null strict-SemVer string and every pinned secondary must AGREE. DEV: those keys must all be ABSENT. present-null is distinguished from absent via an explicit sentinel. A blocked:true contract, or any unreadable pinned input, yields exit 2.

func CheckTree added in v0.12.0

func CheckTree(tree LockstepTree, root string) LockstepResult

CheckTree runs the lockstep check the caller chooses over the checkout at root, reading that checkout's own version-location contract and artefact declaration: the check the preview and the cut run at the dev polarity over the source tree, and the payload render at the public polarity over its output, reachable for any tree a person holds — a public checkout, such as a marketplace install or a release source archive, included (itd-69). An artefact declaration that cannot be read is an unreadable input (exit 2).

type LockstepTree

type LockstepTree string

LockstepTree is the polarity the lockstep check runs under.

const (
	// TreeDev requires the version keys ABSENT (adr-19 dev-stays-unversioned).
	TreeDev LockstepTree = "dev"
	// TreePublic requires the primary present and every secondary to agree.
	TreePublic LockstepTree = "public"
)

func ParseLockstepTree added in v0.12.0

func ParseLockstepTree(s string) (LockstepTree, error)

ParseLockstepTree reads a polarity by its name, refusing any other.

type MarketplaceEntry

type MarketplaceEntry struct {
	Name       string     `json:"name"`
	Source     string     `json:"source,omitempty"`
	SourceKind SourceKind `json:"source_kind"`
	// Root is the payload-relative plugin root a local source resolves to; the
	// empty string is the payload root itself (adr-28: the single repo is its
	// own marketplace, so the canonical source is "./").
	Root string `json:"root"`
	// Pin is the archive a SourceArchive entry names; nil for every other kind.
	Pin *ArchivePin `json:"pin,omitempty"`
}

MarketplaceEntry is one plugin listing in the marketplace manifest.

type PageHelp added in v0.11.0

type PageHelp struct {
	Kind         SurfaceKind `json:"kind"`
	Path         string      `json:"path"`
	Name         string      `json:"name,omitempty"`
	Description  string      `json:"description,omitempty"`
	ArgumentHint string      `json:"argument_hint,omitempty"`
	Error        string      `json:"error,omitempty"`
}

PageHelp is one rendered page: its help, or why it would not load.

func RenderPageHelp added in v0.11.0

func RenderPageHelp(root string, ref PageRef) PageHelp

RenderPageHelp renders one page's help from the tree at root, the way the deep tier's subprocess does. A page loads when it is readable UTF-8, any frontmatter block it opens is closed and is a mapping with no duplicated key, and it renders some help: a description, or for a command or an agent the first line of its body. A skill needs a name and a description in its frontmatter.

The tier reads frontmatter only to judge whether a harness would load the page, so it must never be stricter than the YAML it judges: every shape a YAML reader loads as a top-level mapping loads here too — a scalar continued on indented lines, a quoted or non-ASCII key, a block closed by YAML's own document end (iss-2609251902438821).

type PageRef added in v0.11.0

type PageRef struct {
	Kind SurfaceKind `json:"kind"`
	Path string      `json:"path"`
}

PageRef is one page the deep tier asks the runner to render.

type PageRunner added in v0.11.0

type PageRunner func(root string, pages []PageRef) ([]PageHelp, error)

PageRunner renders pages in an isolated subprocess rooted at root, one answer per page.

type ParityChange added in v0.11.0

type ParityChange string

ParityChange is how one path differs between the baseline and the payload.

const (
	ParityAdded   ParityChange = "added"
	ParityChanged ParityChange = "changed"
	ParityRemoved ParityChange = "removed"
)

type ParityEntry added in v0.11.0

type ParityEntry struct {
	Path   string       `json:"path"`
	Change ParityChange `json:"change"`
	// Digest is the payload's SHA-256 for the path; empty when removed.
	Digest string `json:"digest,omitempty"`
	// BaselineDigest is the baseline's SHA-256; empty when added.
	BaselineDigest string `json:"baseline_digest,omitempty"`
}

ParityEntry is one path that differs.

type ParityInput added in v0.11.0

type ParityInput struct {
	// Baseline is the previous release's tag; empty means there is none.
	Baseline string
	// BaselineError is set when the caller could not resolve the baseline at
	// all (the tags could not be listed): the diff refuses with it.
	BaselineError string
	// Unanchored is set when this checkout cannot name or read the previous
	// release from its own tags — a tagless clone of a tree whose CHANGELOG.md
	// dates a release, a shallow one, or one whose newest tag is older than the
	// release CHANGELOG.md dates newest — and says why. Baseline then names the
	// newest release either source names. The diff never reads it as a first
	// launch and never renders it at a tag: it refuses, unless Fetch reads the
	// release's verified asset.
	Unanchored string
	// Fetch, when set, reads the baseline from the tag's release asset first.
	// Nil keeps the diff disk-only.
	Fetch ReleaseAssetFetcher
	// EnvIgnored names the transport-override variables Fetch's client does
	// not honour; the report carries them whenever Fetch is used.
	EnvIgnored []string
}

ParityInput is the caller's half of a parity diff.

type ParityReport added in v0.11.0

type ParityReport struct {
	// Baseline is the release tag measured against; empty for a first launch.
	Baseline string       `json:"baseline"`
	Source   ParitySource `json:"source"`
	// Fetched names every URL fetched to read the baseline, in order, so a
	// network read is never silent.
	Fetched []string `json:"fetched,omitempty"`
	// Note says why the source is what it is: a first launch, a baseline that
	// shipped no payload, or a release asset that was absent.
	Note string `json:"note,omitempty"`
	// Refused is set when the baseline could not be read; the diff is then
	// empty and RefusalReason names why.
	Refused       bool   `json:"refused"`
	RefusalReason string `json:"refusal_reason,omitempty"`
	// Entries are the paths that differ, sorted by path.
	Entries   []ParityEntry `json:"entries"`
	Added     int           `json:"added"`
	Changed   int           `json:"changed"`
	Removed   int           `json:"removed"`
	Unchanged int           `json:"unchanged"`
	// EnvIgnored names the proxy and CA variables the baseline fetch ignored,
	// as `abcd update` records them, so a fetch never ignores one silently.
	EnvIgnored []string `json:"env_ignored,omitempty"`
	// Normalised names the manifests compared with their version stamps removed.
	Normalised []string `json:"normalised,omitempty"`
	// NotCompared names payload paths the baseline cannot carry by construction.
	NotCompared []string `json:"not_compared,omitempty"`
}

ParityReport is one parity diff.

func PayloadParity added in v0.11.0

func PayloadParity(repoRoot string, bundle Bundle, in ParityInput) ParityReport

PayloadParity diffs the resolved payload against the previous release's. It never returns an error: an unreadable baseline is a refusal inside the report, so the preview always has a report to render.

func (ParityReport) Markdown added in v0.11.0

func (rep ParityReport) Markdown() string

Markdown renders the diff as the pre-flight report's parity section: the baseline and where it came from, then every path that differs with its digests.

type ParitySource added in v0.11.0

type ParitySource string

ParitySource names where the baseline was read from.

const (
	// ParitySourceNone — there is no previous release; every path is added.
	ParitySourceNone ParitySource = "none"
	// ParitySourceRenderAtTag — a fresh render of the payload at the tag.
	ParitySourceRenderAtTag ParitySource = "render-at-tag"
	// ParitySourceReleaseAsset — the tag's published plugin archive, verified.
	ParitySourceReleaseAsset ParitySource = "release-asset"
)

type PayloadPrecheck

type PayloadPrecheck struct {
	// Root is the symlink-resolved source tree.
	Root string
	// Dest is the symlink-resolved staging directory. Nothing is created here.
	Dest string
	// VersionLocationPath is the absolute path to the adr-19 contract.
	VersionLocationPath string
	// PrimaryPath and PrimaryPointer are the pinned primary version location the
	// contract selected, repo-relative and RFC-6901.
	PrimaryPath, PrimaryPointer string
	// Bundle is the payload resolution the render would write from.
	Bundle Bundle
	// Smoke is the light installability tier over the RESOLVED bundle — the same
	// assertions the render makes over its written output, made early enough to
	// refuse before any durable write.
	Smoke SmokeReport
	// DeepSmoke is the deep installability tier, when the caller asked for it.
	DeepSmoke *DeepSmokeReport
	// Parity is the diff against the previous release's payload, when the
	// caller asked for it.
	Parity *ParityReport
	// Gates are every gate the precheck ran over the resolved bundle — the scan,
	// the smoke and the pre-flight suite — in the shape the preview reports.
	Gates []GateSummary
	// Refusals are every reason the precheck refused, collected from all of its
	// gates before it decided; Warnings are the warn-tier concerns.
	Refusals []string
	Warnings []string
	// AllowDirty records that the dirty-tree gate was waived, and Dirty the
	// uncommitted paths it saw — the override the pre-flight report records.
	AllowDirty bool
	Dirty      []string
}

PayloadPrecheck is everything a render refuses on WITHOUT knowing the version: the resolved locations, the payload the render would write, and the light installability verdict over it.

It exists as a named result because the ship verb writes a durable release record (the dated CHANGELOG heading) before it has a version to stamp, and a render that refuses after that write leaves a release in flight that can never be retried. Separating the version-free half lets the caller prove the render will be ACCEPTED before it writes anything.

func PrecheckPayload

func PrecheckPayload(repoRoot, dest string, opts PrecheckOptions) (PayloadPrecheck, error)

PrecheckPayload resolves the release payload and runs every refusal a render makes that does not depend on the version.

It performs ZERO writes to the repository or the destination — not even the destination directory; the deep tier and the parity render write only private temporary trees they remove — so a caller may run it speculatively and a refused cut leaves the filesystem exactly as it found it. RenderPayload runs it as its own first step, so the two can never disagree about what is refusable.

It refuses at once, as a structural fault, when: dest overlaps repoRoot or is already populated; the version-location contract is unreadable or blocked (adr-19 — a blocked decision has no schema-valid place to write); the launch config is malformed; or the bundle carries a violation. Over a bundle it can read, it then runs every gate and refuses with ALL of their findings at once (a *PrecheckRefusal): the secret/PII scan, the pre-flight suite (marker blocks, change narration, the dirty tree per opts.Dirty, and the warn tier when the repository configures it strict), either manifest missing from the payload, and a declared surface the payload does not carry; and, when the caller asks for them, a page the deep smoke tier cannot load and a parity baseline that cannot be read.

func (PayloadPrecheck) PreflightReport added in v0.11.0

func (p PayloadPrecheck) PreflightReport(at time.Time, version string) PreflightReport

PreflightReport is the cut's pre-flight record, for the version the cut will carry (empty when the cut has not derived one yet).

type PayloadRenderRequest

type PayloadRenderRequest struct {
	// RepoRoot is the source tree the payload is cut from. It is read only.
	RepoRoot string
	// Dest is the staging directory the payload is written to. It must live
	// OUTSIDE RepoRoot and be empty or absent.
	Dest string
	// Version is the derived release version, strict SemVer with no leading "v".
	// It is an input rather than something read back out of the tree — the tree
	// has no version to read, which is the whole point of adr-19.
	Version string
	// Entry is the marketplace changelog record for this release.
	Entry ChangelogEntry
	// Dirty is how the render's dirty-tree gate treats uncommitted changes.
	// The zero value refuses them, so a caller that states no policy fails
	// closed; a caller that renders after its own writes, having run the gate
	// before them, states DirtySkip.
	Dirty DirtyPolicy
}

PayloadRenderRequest is the input to RenderPayload.

type PayloadRenderResult

type PayloadRenderResult struct {
	// Dest is the staging directory the payload was written to: the resolved,
	// absolute working value the archive step packs from. It never reaches
	// machine output (iss-81); DisplayDest is what a report names.
	Dest string `json:"-"`
	// DisplayDest is Dest as a report names it (fsutil.RepoRelativePath): relative
	// to the repository inside it, the home redacted to "~" outside it — and a
	// destination is always outside it (iss-2609261848338673).
	DisplayDest string `json:"dest"`
	// Version is the version stamped at every pinned location.
	Version string `json:"version"`
	// Bundle is the resolution the payload was written from, so a caller can
	// render the same file classification a dry-run would.
	Bundle Bundle `json:"bundle"`
	// Files counts the payload files written.
	Files int `json:"files"`
	// Manifests names the manifests that were version-stamped, repo-relative.
	Manifests []string `json:"manifests"`
	// Lockstep is the public check over the rendered payload — the proof that
	// what was written is internally consistent.
	Lockstep LockstepResult `json:"lockstep"`
	// Smoke is the light installability check over the rendered payload — the
	// proof that what was written would actually install.
	Smoke SmokeReport `json:"smoke"`
}

PayloadRenderResult is a completed render.

func RenderPayload

func RenderPayload(req PayloadRenderRequest) (PayloadRenderResult, error)

RenderPayload writes the release payload to req.Dest with the derived version stamped into its manifests, and proves the result with CheckLockstep.

It resolves the payload through the SAME ResolveBundle/LoadIncludes machinery the dry-run and ship gates use, so there is exactly one notion of "what ships"; the render adds only the version stamp on the way out. It never touches the source tree: every write lands under Dest.

Everything refusable that does not need the version is delegated to PrecheckPayload, so a caller may run those refusals FIRST; on top of them this refuses when the version is not strict SemVer or the changelog entry is incomplete.

type PayloadTree

type PayloadTree interface {
	// Has reports whether a FILE exists at rel, a payload-relative,
	// slash-separated path. A path escaping the payload is never present.
	Has(rel string) bool
	// Read returns the bytes at rel, guarded against oversized input.
	Read(rel string) ([]byte, error)
	// List returns every file at or beneath the directory dirRel, sorted, as
	// payload-relative slash paths. An empty result means "no such directory".
	List(dirRel string) []string
}

PayloadTree is the read side of a payload: the only thing surface resolution needs from "the thing that would ship". Two implementations satisfy it — a resolved bundle (nothing materialised) and a rendered directory — so the same resolver serves the light tier and itd-66's deep tier unchanged.

func NewBundleTree

func NewBundleTree(b Bundle) PayloadTree

NewBundleTree views a resolved bundle as a payload tree, so the light tier can assert against WHAT WOULD SHIP without writing anything. A file present in the working tree but excluded from the payload is absent here — which is exactly the bug this gate catches.

func NewDirTree

func NewDirTree(root string) PayloadTree

NewDirTree views a rendered payload directory as a payload tree.

type PluginArchive added in v0.10.0

type PluginArchive struct {
	// Name is the release asset's file name, <plugin>-plugin-v<version>.zip.
	Name string `json:"name"`
	// Path is where the archive was written: the absolute working value a
	// caller removes or reads the archive through. It never reaches machine
	// output (iss-81); DisplayPath is what a report names.
	Path string `json:"-"`
	// DisplayPath is Path as a report names it (fsutil.RepoRelativePath): relative
	// to the repository when --out is inside it, the home redacted to "~"
	// otherwise (iss-2609261950077063).
	DisplayPath string `json:"path"`
	// SHA256 is the lower-case hex digest of the archive's bytes.
	SHA256 string `json:"sha256"`
	// Version is the release version stamped into the archived plugin manifest.
	Version string `json:"version"`
	// Files counts the archive's entries.
	Files int `json:"files"`
	// Bytes is the archive's size.
	Bytes int64 `json:"bytes"`
}

PluginArchive is one rendered, packed plugin release archive.

type PrecheckOptions added in v0.11.0

type PrecheckOptions struct {
	// Dirty is how the dirty-tree gate treats uncommitted changes. The zero
	// value refuses them.
	Dirty DirtyPolicy
	// DocAudit is the documentation audit, measured by the caller. Nil reports
	// the row as not armed.
	DocAudit *DocAuditPreflight
	// DeepSmoke, when set, runs the installability smoke's deep tier through
	// this isolated page runner, over a private materialised copy of the
	// payload it removes. The cut sets it; the archive gate's render does not.
	DeepSmoke PageRunner
	// Parity, when set, diffs the payload against the previous release's and
	// refuses on a baseline that cannot be read.
	Parity *ParityInput
}

PrecheckOptions are the caller's inputs to the pre-flight gate suite the precheck runs.

type PrecheckRefusal added in v0.11.0

type PrecheckRefusal struct {
	// contains filtered or unexported fields
}

PrecheckRefusal is a precheck that refused on one or more gates. It carries every gate's refusal, not the first: the suite runs all of its gates and reports all of their findings in one pass, so one fix pass can clear them. Each refusal stays matchable with errors.Is on its gate's sentinel.

func (*PrecheckRefusal) Error added in v0.11.0

func (r *PrecheckRefusal) Error() string

func (*PrecheckRefusal) Unwrap added in v0.11.0

func (r *PrecheckRefusal) Unwrap() []error

Unwrap exposes every refusal to errors.Is and errors.As.

type PreflightError

type PreflightError struct {
	// contains filtered or unexported fields
}

PreflightError is a payload-preflight config fault: a missing/malformed include config, or an absolute / ".." / denied-rooted include. The caller writes NO manifest and reports the diagnostic (dry-run returns it as its only error case).

func (*PreflightError) Error

func (e *PreflightError) Error() string

func (*PreflightError) Unwrap added in v0.11.0

func (e *PreflightError) Unwrap() error

Unwrap exposes the sentinel a preflight fault carries, when it has one.

type PreflightReport added in v0.11.0

type PreflightReport struct {
	Mode     string        `json:"mode"`
	At       time.Time     `json:"at"`
	Version  string        `json:"version,omitempty"`
	Verdict  string        `json:"verdict"`
	Refusals []string      `json:"refusals"`
	Warnings []string      `json:"warnings"`
	Gates    []GateSummary `json:"gates"`
	// AllowDirty records that --allow-dirty waived the dirty-tree gate, and
	// Dirty every uncommitted path the gate saw.
	AllowDirty bool     `json:"allow_dirty"`
	Dirty      []string `json:"dirty,omitempty"`
	// Parity is the file-level diff against the previous release's payload,
	// and DeepSmoke the deep installability tier, when the run made them.
	Parity    *ParityReport    `json:"parity,omitempty"`
	DeepSmoke *DeepSmokeReport `json:"deep_smoke,omitempty"`
	// Targets lists every planned intent that names a release it must land by
	// and has not shipped (itd-2609212103572513). It is a report, not a gate:
	// it never changes the verdict.
	Targets []TargetedIntent `json:"targets,omitempty"`
}

PreflightReport is one run's pre-flight record: every gate, every refusal, every warning, and the dirty-tree override when one was used.

func (PreflightReport) Markdown added in v0.11.0

func (rep PreflightReport) Markdown() string

Markdown renders the report for a person: the verdict, every refusal, every warning, the override when one was used, and the gate table.

type ReceiptPreflight

type ReceiptPreflight struct {
	// Unreadable, when non-empty, means the measurement could not be taken —
	// the candidate commit would not resolve, or the receipts directory could
	// not be read. A broken measurement is a distinct state from "none found",
	// because reporting the first as the second is a false statement about a
	// real requirement.
	Unreadable string
	// Commit is the candidate commit the receipts would have to name. At preview
	// time this is the working tree's HEAD; the commit release.yml actually arms
	// against is derived from the merge (`<merge>^2^`), so this is indicative,
	// not authoritative — the detail text says so.
	Commit string
	// Recorded names the detectors that have a receipt for Commit, in the order
	// found. It counts receipts present, not receipts valid: validity (PROMOTE,
	// judge model, detector binding, manifest hash) is receipt_gate's to judge,
	// and duplicating that verdict here would be the second trust root this row
	// exists to avoid.
	Recorded []string
}

ReceiptPreflight is the state of the semantic-pass receipts, measured by the caller. A nil pointer means the measurement was not taken, and the gate says so rather than reporting an absence it never looked for.

type RejectedFile

type RejectedFile struct {
	LogicalPath string            `json:"logical_path"`
	Reason      RejectedReason    `json:"reason"`
	Details     map[string]string `json:"details,omitempty"`
}

RejectedFile is a violation.

type RejectedReason

type RejectedReason string

RejectedReason is why a candidate was rejected (any entry fails a ship).

const (
	RejectedDeny            RejectedReason = "deny"
	RejectedSymlinkEscape   RejectedReason = "symlink_escape"
	RejectedSymlinkCycle    RejectedReason = "symlink_cycle"
	RejectedHardlinkDenied  RejectedReason = "hardlink_denied"
	RejectedHardlinkOffrepo RejectedReason = "hardlink_offrepo"
	RejectedDuplicate       RejectedReason = "duplicate"
	RejectedControlChar     RejectedReason = "control_char"
	RejectedPlatformBinary  RejectedReason = "platform_binary"
	RejectedMissingLiteral  RejectedReason = "missing_literal"
	RejectedFSError         RejectedReason = "fs_error"
)

type ReleaseAssetFetcher added in v0.11.0

type ReleaseAssetFetcher interface {
	FetchReleaseAsset(tag, name string) (data []byte, url string, found bool, err error)
}

ReleaseAssetFetcher reads one named asset of a release. found is false, with no error, when the release or the asset does not exist; url is what was requested, for the report. The production fetcher lives at the front door, so this package never opens a connection and a test never needs one.

type RetentionPlan

type RetentionPlan struct {
	Published     string   `json:"published"`
	Line          string   `json:"line"`
	Kept          []string `json:"kept"`
	Pruned        []string `json:"pruned"`
	Refused       bool     `json:"refused"`
	RefusalReason string   `json:"refusal_reason,omitempty"`
}

RetentionPlan is the newest-per-line prune preview for a release cut.

func ComputeRetention

func ComputeRetention(published Semver, existing []Semver) RetentionPlan

ComputeRetention decides newest-per-line retention (brief §3). It is pure and deletes nothing — it renders the prune decision only.

Rules:

  • Each MAJOR.MINOR line keeps only its newest (max-Patch) release.
  • The just-published release is NEVER pruned.
  • If ANY existing release is strictly NEWER than published, refuse and prune nothing (an out-of-order ship the operator must resolve manually).
  • Comparison is core (Major, Minor, Patch) only. Tag string = "v"+version.

type Semver

type Semver struct {
	Major, Minor, Patch int
	Prerelease, Build   string
}

Semver is a parsed strict SemVer 2.0.0 version.

func GitExistingTags

func GitExistingTags(repoRoot string) ([]Semver, error)

GitExistingTags is the default provider for the existing-release list: it runs `git tag --list v*` under repoRoot and parses the strict-SemVer core of each tag. Non-SemVer tags AND prerelease/build tags are ignored — retention decides newest-per-line over RELEASES (core versions), and Tag()/String() render the core only, so admitting a "v1.2.3-rc1" would surface a phantom "v1.2.3" in the plan and collapse it against the real "v1.2.3". It is best-effort — an error yields no tags.

func ParseSemver

func ParseSemver(value string) (Semver, error)

ParseSemver parses a strict SemVer string into its components. It returns an error for any non-conforming input (the boundary validator).

func (Semver) Line

func (s Semver) Line() string

Line is the MAJOR.MINOR retention line a version belongs to.

func (Semver) String

func (s Semver) String() string

String renders the core MAJOR.MINOR.PATCH (retention compares core only).

func (Semver) Tag

func (s Semver) Tag() string

Tag renders the v-prefixed git tag for the version core.

type ShipReport

type ShipReport struct {
	Version   string             `json:"version"`
	Bundle    Bundle             `json:"bundle"`
	Scan      scanner.ScanResult `json:"scan"`
	Lockstep  LockstepResult     `json:"lockstep"`
	Retention RetentionPlan      `json:"retention"`
	Smoke     SmokeReport        `json:"smoke"`
	Gates     []GateSummary      `json:"gates"`
	Warnings  []string           `json:"warnings,omitempty"`
	// AllowedDirty is every uncommitted path AllowDirty carried into the cut.
	AllowedDirty []string `json:"allowed_dirty,omitempty"`
	Blocked      bool     `json:"blocked"`
	BlockReasons []string `json:"block_reasons,omitempty"`
	WouldPublish bool     `json:"would_publish"` // true iff all gates pass
}

ShipReport is the outcome of a ship run. It stops at WouldPublish before any network/publish (no GitHub Release, SLSA, tag push, or retention execution).

func Ship

func Ship(req ShipRequest) (ShipReport, error)

Ship runs the SAME gates as DryRun but HARD-FAILS: a scanner hard-fail, any bundle rejected[] entry, a lockstep drift/unreadable contract, a retention refusal, or a hard-fail in the pre-flight gate suite sets Blocked=true and returns ErrShipBlocked. If every gate passes it stops HERE and returns WouldPublish=true with NO network call — the real GitHub Release + SLSA + tag push + retention prune are a later phase (itd-72, itd-70).

AllowDirty waives the dirty-tree gate only; it must NOT bypass lockstep (adr-20), which never consults it.

Ship has no production caller: the shipped cut is `abcd launch ship`, whose render path runs the same suite through PrecheckPayload. It is kept as the suite's whole-verdict form, the shape itd-72's publishing step will call.

type ShipRequest

type ShipRequest struct {
	RepoRoot string
	// Version is the release version, supplied by the caller for the reason
	// DryRunRequest.Version states: adr-19 leaves nothing in the tree to read.
	Version string
	// AllowDirty is --allow-dirty: an uncommitted change is carried rather than
	// refused, and the report names every path it carried. It waives the
	// dirty-tree gate and nothing else — never lockstep (adr-20).
	AllowDirty   bool
	ExistingTags []Semver
	// DocAudit is the documentation audit, measured by the caller
	// (DryRunRequest.DocAudit states why).
	DocAudit *DocAuditPreflight
}

ShipRequest is the input to a ship run.

type SmokeFinding

type SmokeFinding struct {
	Kind   string `json:"kind"`
	Path   string `json:"path,omitempty"`
	Detail string `json:"detail"`
}

SmokeFinding is one reason the payload would not install.

type SmokeReport

type SmokeReport struct {
	Tier    SmokeTier      `json:"tier"`
	OK      bool           `json:"ok"`
	Surface InstallSurface `json:"surface"`
	// Checked counts the assertions actually made, so a pass over a payload that
	// declared nothing is visibly vacuous rather than reassuring.
	Checked  int            `json:"checked"`
	Findings []SmokeFinding `json:"findings,omitempty"`
}

SmokeReport is one installability check.

func SmokeLight

func SmokeLight(tree PayloadTree) SmokeReport

SmokeLight runs the light installability tier over a payload.

It never returns an error: an unreadable manifest is the most serious FINDING it can make, and reporting it as a finding keeps the gate's output shape the same whether the payload is perfect or unparseable — the dry-run preview depends on always having a report to render.

type SmokeTier

type SmokeTier string

SmokeTier names how deep an installability check went, so a report never implies more assurance than it earned.

const SmokeTierDeep SmokeTier = "deep"

SmokeTierDeep is the light tier's assertions plus every declared page's help rendered in an isolated subprocess.

const SmokeTierLight SmokeTier = "light"

SmokeTierLight is manifest-parse + source-resolve + declared-path-exists.

type SourceKind

type SourceKind string

SourceKind classifies a marketplace entry's source.

const (
	// SourceLocal — a path inside the payload; resolvable offline.
	SourceLocal SourceKind = "local"
	// SourceExternal — a remote or non-path source; resolving it needs the
	// network, so no offline gate asserts against it.
	SourceExternal SourceKind = "external"
	// SourceMissing — the entry declares no source at all.
	SourceMissing SourceKind = "missing"
	// SourceArchive — a pinned release archive ({"source": "archive", "url",
	// "sha256"}). The archive is rendered from this payload's root (archive.go),
	// so offline it resolves to the payload root, exactly like "./"; the digest
	// it pins is judged by the release gate, which re-renders the archive.
	SourceArchive SourceKind = "archive"
)

type SurfaceEntry

type SurfaceEntry struct {
	Kind        SurfaceKind        `json:"kind"`
	Path        string             `json:"path"`
	Origin      SurfaceOrigin      `json:"origin"`
	Requirement SurfaceRequirement `json:"requirement"`
	// DeclaredAs is the raw declaration this entry was expanded from, when it
	// differs from Path (a directory declaration, a hook command string).
	DeclaredAs string `json:"declared_as,omitempty"`
	// Reason explains a requirement other than RequirePayload.
	Reason string `json:"reason,omitempty"`
}

SurfaceEntry is one declared piece of the installable surface.

type SurfaceKind

type SurfaceKind string

SurfaceKind is one register of the installable surface.

const (
	SurfaceCommand SurfaceKind = "command"
	SurfaceAgent   SurfaceKind = "agent"
	SurfaceSkill   SurfaceKind = "skill"
	SurfaceHook    SurfaceKind = "hook"
)

type SurfaceOrigin

type SurfaceOrigin string

SurfaceOrigin records HOW an entry came to be declared. It is carried on every entry so a stricter tier can weigh the registers differently without re-resolving them.

const (
	// OriginConvention — found by auto-discovery under a well-known directory.
	OriginConvention SurfaceOrigin = "convention"
	// OriginManifest — named by an explicit plugin.json key.
	OriginManifest SurfaceOrigin = "manifest"
	// OriginHookCommand — referenced by a hook's command string.
	OriginHookCommand SurfaceOrigin = "hook-command"
)

type SurfaceRequirement

type SurfaceRequirement string

SurfaceRequirement says who is expected to supply an entry's path.

const (
	// RequirePayload — the payload must carry it; absence is a failed install.
	RequirePayload SurfaceRequirement = "payload"
	// RequireInstalled — the install supplies it, so the payload never carries
	// it and its absence proves nothing.
	RequireInstalled SurfaceRequirement = "installed"
)

type TargetMove added in v0.12.0

type TargetMove struct {
	// ID is the intent's id (itd-N).
	ID string `json:"id"`
	// Path is the record's repo-relative path.
	Path string `json:"path"`
	// From is the target the record carried before the cut: `next`, which
	// named the release being cut, or a tag at or below it.
	From string `json:"from"`
}

TargetMove is one targeted intent a cut passes without shipping it: the row the cut rewrites to `next` in the change that rolls the changelog, and the changelog's move note names (criterion 3, ruling BS1 of 2026-09-29).

func MissedTargets added in v0.12.0

func MissedTargets(targets []TargetedIntent, nextTag string) []TargetMove

MissedTargets returns every target the cut to nextTag passes, in the order given: `next`, which names the release being cut, and a tag at or below nextTag, which names this release or one that will now never be cut. Each becomes `next` — the following release, whatever version it derives — so a missed target never goes stale and is never renumbered by guess (the product thinker's ruling BS1 of 2026-09-29). A tag above nextTag is still ahead and is not listed; neither is a value that is not a target (the record lint names it), nor anything when nextTag is empty, because a refused cut derives no version and writes nothing.

type TargetedIntent added in v0.12.0

type TargetedIntent struct {
	// ID is the intent's id (itd-N).
	ID string `json:"id"`
	// Path is the record's repo-relative path.
	Path string `json:"path"`
	// Target is the `target_release` value as the record carries it.
	Target string `json:"target_release"`
	// Invalid is why Target is not a legal target, empty when it is. The row is
	// listed either way: the report never drops a record, and the record lint is
	// what refuses the value.
	Invalid string `json:"invalid,omitempty"`
}

TargetedIntent is one planned intent that names a release it must land by: the row the preview, the cut and their pre-flight reports list. Every field comes out of a record, so a front door sanitises each before it reaches a terminal.

Directories

Path Synopsis
Package scaffold renders and writes the changelog-driven release machinery — release.yml, auto-release.yml, and the adr-37 runbook — into a managed repo that lacks it (itd-93, spc-14).
Package scaffold renders and writes the changelog-driven release machinery — release.yml, auto-release.yml, and the adr-37 runbook — into a managed repo that lacks it (itd-93, spc-14).

Jump to

Keyboard shortcuts

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