surface

package
v0.6.9 Latest Latest
Warning

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

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

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

View Source
const SchemaVersion = 1

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.

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

This section is empty.

Functions

func Encode

func Encode(s Snapshot) ([]byte, error)

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.

Types

type Break

type Break struct {
	Kind    BreakKind `json:"kind"`
	Surface string    `json:"surface"`
}

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

func Diff(base, current Snapshot) []Break

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

func (b Break) String() 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 Command

type Command struct {
	Path   string `json:"path"`
	Hidden bool   `json:"hidden"`
	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

type ManifestEntry struct {
	File string `json:"file"`
	Key  string `json:"key"`
}

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 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, which is exactly the removal the guardrail exists to catch.

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

func Decode(data []byte) (Snapshot, error)

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.

Jump to

Keyboard shortcuts

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