Documentation
¶
Overview ¶
Command apisurface counts the library's exported surface and holds it against the ledger.
The requirement is that CI reports the exported-identifier count delta on every change, and the failure it exists to catch is an exported identifier arriving without a row in docs/api-surface.md. Exported symbols are permanent; the ledger is where the argument for each one is written down, and a count that nothing checks is a count that drifts.
What is counted ¶
Types, funcs, methods, consts and vars declared at package level and exported, plus the exported fields of exported structs, in the two packages the module exports. Interface methods are not counted separately: the ledger gives an interface one row and describes its method in that row, so counting the method again would report a delta the ledger cannot express.
What is compared, and why counts were not enough ¶
The counts, and the set of exported NAMES per package.
The names arrived after a measured failure. A count is a projection, and a gate's enforcement surface is exactly the projection it compares: the P3 slice renamed 61 interfaces onto the house prefix — every one of them gained a leading I — and then named 374 parameters, and both times this tool printed
live 56/56 53/53 109/109 (measured/ledger) the surface matches the ledger
because a rename changes no count and a parameter name changes no count. The enforced ledger would have passed with every symbol row in it stale, twice, on the same day, and nothing would have said so. Both times the rows were corrected by hand and by reading, which is the only reason they are right (docs/lab/2026-09-02-p3-cs1-rename/entry.md and .../2026-09-02-p3-cs2-names/entry.md, and lessons.md "A gate that reads counts cannot see a spelling").
Names are a projection this ledger already carries — §1 through §6 list every symbol by name in their own tables — so comparing them adds no field to §0 and derives nothing. That matters, because §0's rule is that it holds measurements and nothing derived, and the fix for a blind gate must not be a stored copy of a computed number. This reader takes the names the document already states and holds them against the source.
Names, not signatures. A parameter name is inside a signature the ledger writes out in full, and comparing those would mean parsing Go out of a markdown cell and re-deciding what counts as the same type — a much larger promise, and one whose failures would be about formatting. So CS-2's half of the P3 blindness is still on a human, and this doc comment is where that is admitted rather than in nothing.
A method is named the way the ledger spells it: the receiver type without its pointer or type arguments, a dot, the method. `(*App[S]).Handler()` is App.Handler on both sides.
live is compared in both directions, as its counts are: a measured name with no row is an identifier nobody argued for, and a ledgered name that no longer exists is exactly the stale row a rename leaves behind. live/livetest is a ceiling in names as in counts — its ledger describes a v0.1 target surface whose unimplemented symbols are meant to be listed and not yet measured — so only growth past the ledger fails there.
Why the two packages are checked differently ¶
live is complete, so its measured counts must equal the ledger's exactly and any difference in either direction fails.
live/livetest is deliberately partial — the ledger describes the v0.1 target surface and most of its symbols wait on the benchmark harness that fixes their shape. Requiring equality there would mean either a red build for a year or a ledger that lies about the target. So growth is what fails: measured may be below the ledger and may never exceed it, because exceeding it is exactly the "identifier without a row" case.
Why the ledger's table is read whole ¶
This tool refuses a counts table containing a cell or a row it does not read, rather than skipping it. The first version matched two label patterns anywhere in the file and consumed the first two numeric cells of each, so the table's shape was never its business: a third column could hold any number and the tool still reported that the surface matched the ledger. That is the hand-maintained-number failure this program exists to end, reproduced inside the program. Reading the table whole means the only way to add a cell is to decide what reads it.
Usage:
go run ./apisurface # report the counts and check them go run ./apisurface -report # report only, exit 0