Documentation
¶
Overview ¶
Package surface models abcd's public compatibility surface — the commands, flags, and manifest entries a consumer can bind to — as a transport-agnostic value that is snapshotted, committed, and diffed between releases (itd-73, spc-10). The snapshot is the artefact the release guardrail compares: a removed command, a removed flag, a flag that becomes required, or a removed manifest entry is a structural break that a release must declare.
It lives under internal/core, not beside the CLI front door, because the snapshot is DATA and the guardrail that diffs two of them is domain logic: neither may depend on cobra. The walk that reads the live command tree needs cobra, so it lives in internal/surface/cli and hands its result in here. The dependency never points the other way. The package name shares a word with the internal/surface/* front-door tier and nothing else: that tier is about transports, this package is about what those transports expose.
Determinism is this package's contract, not a convenience. The snapshot is a committed file gated by a drift test, so the same tree must encode to the same bytes on every machine and every run. Every collection is sorted by a stable key, nothing is derived from map iteration order, and nothing reads the clock or the environment.
Index ¶
- Constants
- Variables
- func ComposeAppendix(paths []string, tree []Command) string
- func DriftLines(committed, regenerated string) (missing, stale []string)
- func Encode(s Snapshot) ([]byte, error)
- func ExampleFor(path string) (string, bool)
- func ExamplePaths() []string
- func HostDelegatedSentence(path string) string
- func PlacementChanges(committed, current Snapshot) []string
- func RenderChapter(text, appendix string) (string, error)
- func SentenceChanges(committed, current Snapshot) []string
- func SentenceFor(path string) (string, bool)
- func SentencePaths() []string
- func SplitChapter(text string) (string, error)
- func UnbuiltSentence(path string) string
- type Break
- type BreakKind
- type Chapter
- type Command
- type Flag
- type ManifestEntry
- type RegeneratedChapter
- type RegisterRow
- type Sentence
- type ShapeClaim
- type Snapshot
Constants ¶
const ( AppendixBegin = "" /* 127-byte string literal not displayed */ AppendixEnd = "<!-- surface-appendix:end -->" )
AppendixBegin and AppendixEnd delimit the generated region. Each must stand alone on its line. The generator refuses a chapter whose markers are absent, crossed, duplicated, misspelt, fenced, or followed by more prose, rather than guessing where the region is.
const BriefSurfacesDir = ".abcd/development/brief/04-surfaces"
BriefSurfacesDir is the brief's surface-chapter directory, repo-relative and slash-separated. Its README.md is the surfaces register: the table whose rows map each user-facing command to the chapter documenting it.
const RegisterFile = "README.md"
RegisterFile is the register's basename inside BriefSurfacesDir.
const SchemaVersion = 4
SchemaVersion is the version of the snapshot's on-disk shape. It is written into every encoded snapshot and checked on decode, so a guardrail can never silently compare two snapshots written to different shapes — a mismatch there would report phantom breaks or, worse, miss real ones.
Version 2 added each command's help placement (Command.Group and Command.Block, itd-146); version 3 added each command's sentence (Command.Sentence, itd-2609212113220149); version 4 added the successor of a moved spelling (Command.MovedTo, itd-2609212130136102).
const SentenceCap = 160
SentenceCap is the most characters a sentence may run to (spc-2609212139583822 scope 2). It counts characters, not bytes, because a reader counts characters and a `×` or a `—` is one of them.
const SnapshotPath = ".abcd/development/release/surface.json"
SnapshotPath is where the committed snapshot lives, repo-relative and slash-separated.
It sits with the type rather than with the front door that generates the file, because the release guardrail reads the baseline out of a git TREE (`ls-tree`/ `cat-file` take slash-separated repo-relative paths) and must name the same location the generator writes and the drift test gates. One constant is what keeps a guardrail from silently reading a path nothing writes and concluding there is no baseline.
Variables ¶
var ( ErrMarkerAbsent = errors.New("the surface appendix markers are absent") ErrMarkerCrossed = errors.New("the surface appendix end marker precedes its begin marker") ErrMarkerDuplicated = errors.New("a surface appendix marker appears more than once") ErrMarkerInFence = errors.New("a surface appendix marker sits inside a fenced code block") ErrMarkerMalformed = errors.New("a surface appendix marker is misspelt") ErrMarkerNotAtEnd = errors.New("prose follows the surface appendix end marker; the appendix sits at the end of the chapter") ErrChapterWithoutRow = errors.New("a surface chapter has no row in the surfaces register") ErrRowWithoutChapter = errors.New("a surfaces register row names a chapter that does not exist") // ErrShippedWithoutSurface refuses a register row that reads shipped for a // command the tree registers neither as a verb nor as a root flag and the // record-lint host_delegated list does not name: the generator has no true // sentence for it, and labelling it by elimination is how /abcd:version came // to be called host-delegated (iss-2609302306003610). ErrShippedWithoutSurface = errors.New("a surfaces register row reads shipped for a command the tree registers as neither a verb nor a root flag, and the host_delegated list does not name it") )
The refusals a malformed chapter earns. Each is wrapped with the chapter's name and the offending line, so a failure points at the file to fix.
Functions ¶
func ComposeAppendix ¶ added in v0.10.0
ComposeAppendix renders the generated region for a chapter documenting the given command paths, in the order given (the register's order). Each shipped command is listed with every descendant, depth-first by path, each with its direct sub-verbs and its own flags — flags a command declares, never those it inherits, so a persistent flag appears once, where it is declared. The bare root is the exception: its children are verbs with chapters of their own, so its section lists its flags alone.
The inputs are the path, the flags and the sub-verbs, and nothing else, so an exit code or an output field cannot reach the block until the tree records it somewhere this function is handed.
func DriftLines ¶ added in v0.10.0
DriftLines compares a committed chapter with its regenerated form line by line as multisets: missing holds the lines the regenerated chapter has and the committed one lacks (a claim the tree makes that the chapter does not), stale the reverse (a claim the chapter makes that the tree no longer does).
func Encode ¶
Encode renders the snapshot as the committed artefact: canonical order, fixed key order, two-space indent, and exactly one trailing newline.
It canonicalises defensively rather than trusting the caller to have gone through NewSnapshot, because Encode is the boundary that writes a file a drift test then gates — a snapshot that encodes differently depending on how it was built would turn that gate into a coin flip. HTML escaping is off so a key containing `<`, `>`, or `&` is written literally instead of as an escape that a human reader would have to decode.
func ExampleFor ¶ added in v0.11.1
ExampleFor returns the worked example the manifest declares for the command at path ("abcd capture resolve"), and whether it declares one.
func ExamplePaths ¶ added in v0.11.1
func ExamplePaths() []string
ExamplePaths returns every command path the manifest declares an example for, sorted.
func HostDelegatedSentence ¶ added in v0.12.0
HostDelegatedSentence is the whole appendix of a chapter whose register row reads shipped while the command tree registers no verb for it, and which the record-lint host_delegated list names: a host-delegated command, which ships as a command page the host carries out (iss-2609231931006041). Saying there is no shipped surface there would contradict the register row it pairs with.
func PlacementChanges ¶ added in v0.11.0
PlacementChanges names every command whose help group or block differs between committed and current, one line each in canonical path order: "abcd capture: group records → checks".
It exists because a byte comparison of two snapshots can say only THAT they differ. The drift test and the release gate's stale-surface refusal both call it, so a verb moved between groups without regenerating the snapshot is named rather than left for the operator to find in a file of several thousand lines (itd-146 criterion 4). A command present on one side only is an addition or a removal, which Diff and the drift test already report; it is not a placement change and is not listed.
func RenderChapter ¶ added in v0.10.0
RenderChapter replaces the bytes between the markers with appendix and leaves every byte above the opening marker as it was. The chapter ends with the closing marker and one newline.
func SentenceChanges ¶ added in v0.11.0
SentenceChanges names every command whose sentence differs between committed and current, one line each in canonical path order: "abcd capture: sentence reworded". It is PlacementChanges' twin for the sentence field: Diff never reads a sentence, because a rewording changes no invocation, so the byte comparison behind the drift test and the release gate's stale-surface refusal is the only thing that sees one, and naming the command is what makes either actionable. A command present on one side only is an addition or a removal, which Diff and the drift test already report, and is not listed.
func SentenceFor ¶ added in v0.11.0
SentenceFor returns the sentence the manifest declares for the command at path ("abcd capture list"), and whether it declares one.
func SentencePaths ¶ added in v0.11.0
func SentencePaths() []string
SentencePaths returns every command path the manifest declares a sentence for, sorted.
func SplitChapter ¶ added in v0.10.0
SplitChapter returns the hand-written prose above the opening marker, byte for byte, or the refusal that names why the region cannot be located.
func UnbuiltSentence ¶ added in v0.10.0
UnbuiltSentence is the whole appendix of a chapter whose surface the command tree does not register and whose register row does not read shipped: a staged design target. Every chapter carries a block, so a reader learns the surface is unbuilt from the place they would have read its flags, never from absence.
Types ¶
type Break ¶
Break is one structural incompatibility, named precisely enough to act on.
Surface is the whole point of the type. A gate that reports "a break was detected" tells the operator to go hunting; Surface names the command path, the command-and-flag, or the manifest file and key that changed, in the form the operator reads it in the tree.
func Diff ¶
Diff reports every way current narrows the compatibility surface base declared — the structural half of the release guardrail (spc-10 AC 4).
It is a pure comparison of two values: it reads no git, no files, and no clock, so the same pair of snapshots always yields the same breaks in the same order. Where the two snapshots come from is the CALLER's decision and is where the guardrail is easiest to get wrong: comparing the committed baseline against the current tree compares a file against itself, because a drift test keeps them equal. The baseline must be read out of the last release tag.
What Diff reports is the taxonomy and only the taxonomy:
- a removed or renamed command;
- a removed or renamed flag on a command that still exists;
- a flag that was optional, or absent, becoming required on a command that still exists;
- a removed declared manifest entry.
Additions of any kind are silent — a new command, a new optional flag, a new manifest entry — as are reordering and every change the snapshot does not model at all (help text, descriptions, summaries), which is precisely why the snapshot does not model them.
Two known limits, stated rather than hidden. A flag whose VALUE TYPE changes (string to stringSlice) is a compatibility event the taxonomy does not list, so it passes here and rests on the author's `breaking` judgement. And a behavioural break behind an unchanged surface is invisible to any structural diff. Both are the author's call; the gate backstops the structural ones.
Results are ordered: commands in canonical path order (each command's own flag breaks before the next command's), then manifest entries in canonical order. The whole set is returned, never just the first, so one fix-and-rerun cycle shows the operator everything that changed.
func (Break) String ¶
String renders one break as the line a failing gate names it with. It says "removed or renamed" for the two deletion kinds because a structural diff genuinely cannot tell them apart — the old path is gone either way, and claiming to know which happened would be a guess the operator has to check.
type BreakKind ¶
type BreakKind string
BreakKind classifies one structural incompatibility between two snapshots.
The set is exactly the break taxonomy of spc-10 and plan outcome 5 — nothing more. A kind absent from this list (a changed flag type, a flag losing its shorthand, a command becoming hidden) is deliberately NOT a break: the taxonomy is what a release contract can be held to, and widening it here would make the gate refuse releases the contract permits. Those are named as limits in Diff rather than silently folded in.
The string values are stable identifiers a renderer or a machine-readable preview can switch on, so renaming a constant is safe and changing a value is a contract change.
const ( // BreakCommandRemoved covers a removal AND a rename: a renamed command is a // removal at the path callers type, plus an addition nobody depends on yet. BreakCommandRemoved BreakKind = "command_removed" // BreakFlagRemoved covers a removed or renamed flag, for the same reason. BreakFlagRemoved BreakKind = "flag_removed" // BreakFlagRequired is a flag that was optional or absent becoming // mandatory — the one narrowing that adds surface rather than deleting it, // and the one most easily missed, because the flag is still there. BreakFlagRequired BreakKind = "flag_required" // BreakManifestRemoved is a declared manifest key path that is gone. BreakManifestRemoved BreakKind = "manifest_entry_removed" )
type Chapter ¶ added in v0.10.0
type Chapter struct {
File string
Commands []string
// Shipped holds the commands whose register row reads shipped.
Shipped map[string]bool
// HostDelegated holds the commands the record-lint surface_coverage
// host_delegated list names, as command paths (`abcd consult`).
HostDelegated map[string]bool
}
Chapter is one surface chapter and the command paths it documents, in register order.
func Chapters ¶ added in v0.10.0
func Chapters(rows []RegisterRow, files []string) ([]Chapter, error)
Chapters pairs the chapter files with the register's rows. Every chapter file must be named by at least one row, and every row naming a chapter must name a file that exists: a chapter the register does not know has no command to derive an appendix from. Each mismatch is refused by name, and every chapter that does pair is still returned, so one unfinished chapter never hides the rest; the error joins every refusal.
func (Chapter) Appendix ¶ added in v0.12.0
Appendix renders the chapter's generated region against tree. It is ComposeAppendix with the register's word on each command. A command the tree does not register as a verb is unbuilt when its row does not read shipped; when it does, it is the root flag named for it if the root declares one, host-delegated if the host_delegated list names it, and unaccounted otherwise (which RegenerateChapters refuses).
func (Chapter) Unaccounted ¶ added in v0.13.0
Unaccounted returns the chapter's commands whose register row reads shipped while the tree registers them as neither a verb nor a root flag and the host_delegated list does not name them.
type Command ¶
type Command struct {
Path string `json:"path"`
Hidden bool `json:"hidden"`
// Group is the help group a visible top-level verb is listed under
// (itd-146): "set-up", "records", "checks", "portability", "release", or
// "agents" for the agents-and-hosts block. Empty for a sub-command and for
// a hidden command, and omitted from the encoding when empty.
Group string `json:"group,omitempty"`
// Block is which of the two help blocks lists the command: "people" or
// "agents". A visible top-level verb always carries one; a sub-command
// carries one only when it is listed in its own right, which today means a
// sub-verb of the agents block (`guard hook`). Omitted when empty.
//
// Neither field is a compatibility claim. Diff never reads them, because a
// regroup changes no invocation; they are here so that a regroup is VISIBLE
// in the committed tree, its drift test and the release gate's stale-surface
// refusal, which is where PlacementChanges names it.
Block string `json:"block,omitempty"`
// Sentence is the command's one sentence (itd-2609212113220149): what it
// does, what it writes, and when it refuses, as the manifest in
// sentences.go declares it and the help, the agents block and the plugin
// page render it. Omitted when the command has none, which is every hidden
// command. Like the placement it is no compatibility claim: Diff never
// reads it, because rewording a sentence changes no invocation.
Sentence string `json:"sentence,omitempty"`
// MovedTo is where a moved spelling went (itd-2609212130136102): the
// invocation that does what this one did, such as "abcd lint docs" for
// "abcd docs lint" or "abcd ahoy --dry-run" for "abcd ahoy dry-run". The
// old spelling stays in the tree for one release as a stub that answers
// with its successor and exits non-zero, so it is still surface a script
// may bind to, and the record of the move is what tells a reader of the
// snapshot where to go. A command with sub-verbs of its own records it when
// its BARE form moved and its sub-verbs did not (`abcd identity`). Empty,
// and omitted from the encoding, for every command that did not move.
// Like the sentence it is no compatibility claim: Diff never reads it.
MovedTo string `json:"moved_to,omitempty"`
Flags []Flag `json:"flags"`
}
Command is one command in the tree, identified by its full command path ("abcd intent plan") because that path is what a caller types and therefore what a rename breaks.
Hidden commands are included. The operator-internal `hook` subtree is hidden from the documentation page but is still public surface for compatibility purposes: harness wiring invokes it by name, so removing or renaming it breaks installations even though no user ever reads about it. Recording Hidden lets the guardrail report what kind of surface changed without letting it skip any.
Hidden is what the command DECLARES, not whether it is reachable in help: a visible subcommand of a hidden parent records Hidden=false, because that is what the tree says and the hiding is the help renderer's doing. The field is descriptive; presence in the snapshot is what decides a break.
type Flag ¶
type Flag struct {
Name string `json:"name"`
Shorthand string `json:"shorthand"`
// Type is the flag's value type as the flag library names it ("bool",
// "string", "stringSlice"). A type change is a compatibility event in its own
// right: `--since` going from string to stringSlice changes what callers may
// pass.
Type string `json:"type"`
// Required records whether the flag must be supplied. Nothing in the tree
// marks a flag required today, so every entry is false; the field exists so
// that a flag LATER becoming required is diffed as the break it is, rather
// than being invisible because the snapshot never modelled requiredness.
Required bool `json:"required"`
// Hidden mirrors Command.Hidden: an undocumented flag is still a flag a
// script may pass.
Hidden bool `json:"hidden"`
}
Flag is one flag declared ON a command — its own flags plus the persistent flags it declares, never the persistent flags it inherits. A persistent flag is therefore recorded exactly once, on the command that declares it, and its removal there is one break rather than one per descendant.
type ManifestEntry ¶
ManifestEntry is one declared key path in one plugin manifest — the unit the guardrail counts as "a manifest surface entry".
An entry is a PRESENCE, not a value. Key paths are leaf paths through the manifest JSON (see ManifestEntries for the flattening rules) and the value is deliberately not recorded, because the break taxonomy makes a removed entry a break while a changed description or a reordered list is not. Recording values would make every prose edit to a manifest look like a surface change and would bury the removals the guardrail exists to catch.
func ManifestEntries ¶
func ManifestEntries(repoRoot string) ([]ManifestEntry, error)
ManifestEntries reads both plugin manifests under repoRoot and flattens them into the set of declared key paths — the manifest half of the surface snapshot.
An ENTRY is one leaf key path, and only its presence is recorded. The flattening rules are:
- an object contributes one entry per leaf beneath it, joined with dots ("author.name"); an empty object contributes one entry at its own path, so that emptying it is still visible;
- an array whose elements are all objects carrying a unique, non-empty "name" contributes entries beneath each element, keyed by that name ("plugins[abcd].source") — so reordering the array changes nothing while removing or renaming a member is a removed entry;
- any other array (scalars such as keywords, unnamed objects, ambiguous duplicate names) contributes exactly one entry at its own path. Keying those by position would report a reorder as a break, and reordering is explicitly not one;
- a scalar, including null, contributes one entry at its own path.
Values are not recorded, deliberately: the break taxonomy makes a removed entry a break and a changed description or a reordered list not one, so carrying values would report every prose edit as a surface change.
Nothing here treats any particular key as expected or required. plugin.json declares no version in the development tree and the rendered release payload adds one; both are ordinary entry sets, so the absence of a version is not an anomaly and its later presence reads as one added entry.
A manifest that is PRESENT and cannot be read or parsed, or whose root is not a JSON object, is an error rather than an empty entry set: reporting "no entries" for a manifest that failed to load would make every declared entry look like surface that was never there.
An ABSENT manifest is not that case. It contributes no entries, because a repo whose artefact is not a plugin — a binary, an application bundle, a library — declares no plugin surface, and treating the absence as an unreadable payload stopped every caller of the snapshot before it ran, `abcd changelog` included (iss-2609100506255436). Where a plugin manifest lives is fixed; whether this artefact has one is a per-repo fact, the same distinction adr-19 already drew for the version location. Nor does absence hide a removal: a manifest that WAS declared at the last release and is gone now yields a manifest_entry_removed break against that baseline, which is the channel the guardrail exists to report through — an error at this seam produced no verdict at all.
type RegeneratedChapter ¶ added in v0.10.0
RegeneratedChapter is one chapter as committed and as the tree would have it.
func RegenerateChapters ¶ added in v0.10.0
func RegenerateChapters(dir string, tree []Command, hostDelegated []string) ([]RegeneratedChapter, error)
RegenerateChapters reads the register and every chapter in dir and returns each chapter regenerated against tree. It writes nothing: the generator writes Want, and the drift test compares it with Committed, so the file written and the file checked come from one code path. A chapter that cannot be regenerated — no register row, or markers absent or malformed — is skipped and refused by name, and every other chapter is still returned: the error joins the refusals, and a nil error means every chapter was regenerated. Only an unreadable register or directory stops the whole walk.
hostDelegated is the record-lint surface_coverage host_delegated list, as verb names (`consult`): the one place a surface is declared host-delegated, so the appendix calls a command that only when the list does.
func (RegeneratedChapter) Drift ¶ added in v0.10.0
func (c RegeneratedChapter) Drift() string
Drift reports how the committed chapter differs from its regeneration, naming the chapter and each claim, or "" when they agree. A line the tree implies and the chapter lacks is missing; a line the chapter carries and the tree no longer implies is stale.
type RegisterRow ¶ added in v0.10.0
type RegisterRow struct {
Command string // command path: "abcd" for the bare `/abcd`, "abcd capture" for `/abcd:capture`
Status string // lower-cased Status cell
Chapter string // the chapter's basename in the register's directory; "" when the row points elsewhere
Line int // 1-based line in the register
}
RegisterRow is one row of the surfaces register.
func ParseRegister ¶ added in v0.10.0
func ParseRegister(text string) []RegisterRow
ParseRegister reads the register's table — the first table whose header names a Command, a Status and a File column. A row whose File link leaves the directory, or carries an anchor, documents its surface outside the chapters and has an empty Chapter.
type Sentence ¶ added in v0.11.0
Sentence is one verb's sentence split into its three clauses, each without its separator.
func ParseSentence ¶ added in v0.11.0
ParseSentence splits s into its three clauses, or refuses it with an error naming the defect: empty, more than one line, over the cap, not one sentence, no closing period, a missing, doubled or out-of-order separator, an empty doing clause or one opening in lower case, a writing clause that does not open with "Writes", or a refusing clause that opens with neither "refuses" nor "never refuses".
type ShapeClaim ¶ added in v0.10.0
type ShapeClaim struct {
Line int // 1-based line in the chapter
Spelling string // the flag, the command path, or the backticked sub-verb name
}
ShapeClaim is one flag or sub-verb stated in a chapter's hand-written prose.
func ProseShapeClaims ¶ added in v0.10.0
func ProseShapeClaims(prose string, own []string, tree []Command) []ShapeClaim
ProseShapeClaims reports every flag and sub-verb of abcd's that the prose states. own is the chapter's command paths. Three spellings count as a claim:
- a flag the command tree registers, spelt long (`--name`) or as its single-dash shorthand (`-n`), anywhere, fenced examples included. A flag the tree does not register is another program's (git's `--force`) or no program's, and is prose;
- a sub-verb's command path below its top-level verb (`capture list`), for every sub-verb the tree registers, when it is written as an invocation: inside a code span or fence, or prefixed with `abcd ` or `/abcd:`. The same words as plain English ("the intent plan", "the docs lint") are prose;
- a backticked sub-verb name below one of the chapter's own verbs (`list` in the capture chapter).
The `## Sub-verbs` section's table and its standard blockquote note are exempt: the table is compared against the command-tree snapshot by surface_coverage and carries the adr-40 bucket, which the tree does not record, so it is a checked register rather than a hand-written shape claim; the note names the bucket vocabulary, some of whose words are also sub-verb names. Any other prose in that section is checked like the rest. A plain word ("listing", "the cut") is not a spelling and is how prose refers to a behaviour.
type Snapshot ¶
type Snapshot struct {
SchemaVersion int `json:"schema_version"`
Commands []Command `json:"commands"`
Manifest []ManifestEntry `json:"manifest"`
}
Snapshot is the whole compatibility surface at one commit.
The field order is the JSON key order: encoding/json emits struct fields in declaration order, so the shape on disk is fixed by this declaration and by nothing else. Maps are deliberately absent from the whole type — a map would make the encoded bytes depend on iteration order.
func Decode ¶
Decode reads a committed snapshot back, so a guardrail can diff the released baseline against the current tree.
It is strict in every direction. Unknown fields are rejected because a field this binary does not understand means the file describes surface it cannot compare, and a schema version other than the one this binary writes is rejected for the same reason. Trailing content is rejected because the decoder is a STREAM decoder: it stops at the first value and would otherwise accept a blob that is a snapshot followed by anything at all. All three are fail-closed on purpose: a baseline that parses "successfully" into a partial or empty surface would make every removal look like a surface that never existed.
func NewSnapshot ¶
func NewSnapshot(commands []Command, entries []ManifestEntry) Snapshot
NewSnapshot is the one constructor: it stamps the schema version and returns the canonical form of the surface it is given.
Callers hand it whatever order their walk produced — cobra's registration order, a directory read, a map range — and canonicalisation here is what makes two runs over the same tree byte-identical. The input slices are copied, so a caller that keeps mutating its working slices cannot reorder a snapshot after the fact.