Skip to content

feat(machine): a pack cannot name the runtime, and the compiler is what says so (#514) - #579

Merged
stephrobert merged 3 commits into
mainfrom
feat/514-driver-unnameable
Aug 28, 2026
Merged

feat(machine): a pack cannot name the runtime, and the compiler is what says so (#514)#579
stephrobert merged 3 commits into
mainfrom
feat/514-driver-unnameable

Conversation

@stephrobert

Copy link
Copy Markdown
Owner

Closes #514.

#514's §2.1 states criterion (a) as a specification: "var d machine.Driver — or
any driver-method expression — in a pack does not compile"
. Two of its three
closures had shipped (Env no longer publishes the driver, Binding.Driver is
no longer an exported field) and they removed every call site. The third had
not. Measured on 154c204, this file dropped into internal/providers/scaleway/:

package scaleway
import "github.com/stephrobert/feint/internal/core/machine"
var _ machine.Driver

go build ./internal/providers/scaleway/exit 0.

The surface was therefore held by TestNoPackReachesPastTheDeclaredDriverSurface
alone — convention plus an AST scan, which §2.1 itself says is never an API
that cannot be bypassed
. The correction of record: the criterion was held one
layer below its promise, and nothing wrote that down.
It was not unheld.

The proof, and it is a test that requires a compilation to fail

internal/cli/testdata/bypass/ — nine packages, eight that must not build,
one that must. TestThePacksCannotNameTheDriver builds each and requires the
failure plus its expected message, so a probe failing for another reason is
not counted:

probe required error
driver/ (the file above, verbatim) undefined: machine.Driver
router/ firewaller/ peerer/ isolator/ balancer/ undefined: machine.<Half>
envdriver/ emulator.Env{}.Machines undefined
bindingdriver/ b.Driver undefined
admitted/ must compile — the positive control, run first, fatal

Registered probes and on-disk directories are checked against each other both
ways. 0.66 s. tools/falsify/specs/driver-unnameable.json: six mutations, all
compile, all bite
— including the harness pointed at a directory that does not
exist, which is the control catching a probe suite that compiles nothing.

The form, and why it is not the one the brief proposed

machine.Runtimea struct with an unexported driver field, built by
machine.Use(d). Not Go's internal/ rule, and not a narrowed interface.

A struct, because an assertion needs no name.
rt.(interface{ Remove(context.Context, string) error }).Remove(ctx, victim)
compiles from anywhere, and that shape is already in this repository twice — so
it would have been copied. Visibility on a name is not visibility on a shape;
only an unexported field makes the two the same. internal/core/machine/internal/x
would also still have admitted any future sibling of internal/core/machine/*,
and would have dragged Attachment, FirewallSpec and BalancerSpec — all in
PackSurface, all required to stay nameable — into the sub-package or behind a
screen of aliases.

Runtime's method set is the operator's half only: Name, Available, Runs,
Capabilities, DeclaredCapabilities, Verify, Survey, Sweeps, Prune,
ReleaseUplink, Traps, Repair, Watch, BuildsImages, LocalImages,
RemovesImages, RemoveImage, BuildImage, Inventory, DerivedInventory.
No Start, Stop, Remove, Attach, Detach, EnsureNetwork,
RemoveNetwork, RouteAddress, EnsureFirewall, PeerNetworks,
EnsureBalancer.

Two incidental wins: five copies of driver.(machine.Noop) collapse into one
Runs(), and every optional-half assertion became a method that keeps the three
outcomes — so "nobody could be asked" can no longer read as "there is
nothing"
.

The three acceptance criteria, judged one by one

(a) — closed by this branch, with a residue named rather than implied.
machine.Noop, machine.Incus and machine.Recorder stay exported, so a
driver-verb expression in a pack still compiles. Measured, not assumed:
machine.Noop{}.Remove(ctx, "feint-scw-x") builds, and
TestNoPackReachesPastTheDeclaredDriverSurface names both reaches with pack,
gesture and line. Closing that means unexporting Incus (~1000 receiver sites)
and taking Noop from twelve test files and from NewRecorder. Read literally,
(a) is closed for the driver interface and its five pack-facing halves, and held
by the scan for the three implementations — and that sentence belongs in the
issue, which is why it is here.

(b) — was already closed by #517, and this branch did not weaken it.
testdata/provider-four compiles and replays green through the contract alone;
provider-four.json's thirteen mutations all still bite; the pack stays in
disciplinedPackDirs and TestTheDisciplineDetectorsReadTheFourthPack still
resolves its ≥40 reaches. Not one line of it was touched.

(c) — now an observation rather than a claim. This was the last thing
missing, and it was missing for a reason that had nothing to do with this branch:
conformance:functional was red on main for the isolation-detach race, fixed
since in #577 and #578. Replayed on this rebased tree, both stacks, zero FAIL:

ok: scaleway: 3 rule set(s) scw-*, 6 reference(s) across 6 machine(s), as declared
ok: outscale: 2 rule set(s) osc-*, 3 reference(s) across 3 machine(s), as declared
conformance: every stack proved what it declares, and every bound it cannot measure was named

The Outscale witness finally has a before and an after. Alongside it: the
fields leg byte-identical before and after (352/375, the same nine declined
fields with the same reasons, 316 operations compared, the same 398/149 blind
spot), both runtime legs green in both modes, and all prescribed falsifications
biting.

Gates

prepush 0 after the rebase · conformance:leg -- probe 0.6 s ·
conformance:leg -- fields 209 s, 352/375 · conformance:environment 0 ·
FEINT_VM=incus-ovn conformance:leg -- runtime 614 s — isolation asserted,
balancer dataplane 6/6 across both machines · FEINT_VM=incus conformance:leg -- runtime 246 s — isolation skipped with its message
(capabilities.isolation false), everything else identical ·
conformance:functional 289 s, both stacks · 45 falsify specs prescribed by
testplan, 45 pass.

The mode split is exactly what machine.Capabilities promises: the bridge
declines isolation and the balancer suite out loud, the rest holds in both.

What was not obtained

  • The residue above, stated rather than closed.
  • /_feint/health has one behaviour change on a path no production code takes: a
    hand-built &emulator.Env{} with no runtime used to publish capabilities: null; a zero machine.Runtime now publishes Noop's declaration (all false).
    DefaultEnv and serve both set Noop explicitly, so nothing observable moved,
    and TestAnUndeclaredDriverIsNotTheSameAsOneThatDeclaresNothing still passes.
  • Four existing falsification specs named fragments this rename moved
    (a-run-ends-where-it-could-start, one-machine-per-address,
    run-leaves-nothing, trapped-station). falsify:lint caught all six dead
    mutations in a millisecond; they are retargeted at the code carrying the same
    guard today.

Where testplan was wrong

It did not ask for the bridge-mode runtime leg, and its own footnote says
"the leg runs under OVN; a bridge is a different verdict for isolation alone".
That sentence is false, and #574 was its counter-example the day before: under the
bridge the verdict differed for the firewall. The bridge run was played
because the brief mandated it, not because the plan asked. That line deserves its
own correction, which is not made here — fixing an instrument's prose without a
control is the pattern this milestone exists to remove.

stephrobert and others added 3 commits August 28, 2026 11:15
…at says so (#514)

machine.Binding's went unexported, and `p.binding().Driver.EnsureNetwork(…)`
stopped compiling. It left the way to *name* one open. Measured on 154c204, in
an isolated worktree: this file, dropped into internal/providers/scaleway/,
compiled and `go build ./internal/providers/scaleway/` exited 0.

	package scaleway
	import "github.com/stephrobert/feint/internal/core/machine"
	var _ machine.Driver

So the surface was held by TestNoPackReachesPastTheDeclaredDriverSurface alone
— a convention plus an AST scan, which is what #514 §2.1 says is not enough,
because a scan is a list somebody can widen and a build error is not.

Driver, Router, Firewaller, Peerer, Isolator and Balancer are unexported now.
That is stronger than the internal/ package rule the issue suggests and costs
no new package: internal/ would still admit any future internal/core/machine/*
sibling, and it would have dragged Spec, NetworkSpec, Attachment and the
firewall and balancer vocabulary along with the interfaces or left a screen of
aliases behind.

What leaves the package instead is machine.Runtime, and it is a struct rather
than a narrowed interface on purpose: an assertion needs no name, so

	rt.(interface{ Remove(context.Context, string) error }).Remove(ctx, victim)

would have undone the whole thing in one line — and that shape is already in
the repository twice, in machineDriver's Verify half and in `feint images
remove`. A struct with an unexported field cannot be asserted on, so its method
set is the whole of what a holder can do. It carries the operator's half —
identity, capabilities, survey, prune, repair, watch, images — and no verb that
moves a machine, a network, an address or a rule set. Those stay with Binding,
Reconciler and GroupSync, where the ownership checks and the one order are.

internal/cli is the caller that legitimately needed a name, and it needed
thirteen in production plus some forty in tests. It holds machine.Runtime
everywhere now, and five copies of `driver.(machine.Noop)` became one
Runtime.Runs(): a question written five times is a question one caller answers
differently. Every optional-half assertion it made — Surveyor, Pruner,
Repairer, Watcher, UplinkReleaser, ImageBuilder, ImageLister and one anonymous
RemoveImage — is a Runtime method keeping the three outcomes, so "nobody could
be asked" never reads as "there is nothing".

The proof is a test that compiles and requires the failure, not a comment
claiming it. internal/cli/testdata/bypass holds nine packages: eight that must
not build, one that must. TestThePacksCannotNameTheDriver builds each and
checks the message names the right symbol; the ninth, admitted/, runs first and
is fatal, because a probe failing for a wrong import or a module boundary reads
exactly like the door being shut. tools/falsify/specs/driver-unnameable.json
plants six mutations — the driver re-exported, the balancing half re-exported,
the routing half re-exported past the ratchet, Env republishing its runtime,
Binding republishing its driver, and the probe harness pointed at a directory
that is not there. All six compile and all six bite.

TestNoPackReachesPastTheDeclaredDriverSurface stays, and mustStayOutside gains
a sibling that inverts: mustNotBeNameable asserts the six are *not* exported
and still exist as unexported interfaces, so re-exporting one fails a named
test instead of quietly putting the boundary back behind a scan.

What this does not close, written down rather than implied: machine.Noop,
machine.Incus and machine.Recorder stay exported — the metadata-only default,
the only runtime, and #515's shared recorder are all needed by name outside the
package — so `machine.Noop{}.Remove(ctx, n)` in a pack still compiles and is
caught by the scan. The two controls are not alternatives.

PackSurface and its exemption ledger are untouched, the ledger is still empty,
machine.NewRecorder and its twenty-one gestures are untouched, and
testdata/provider-four compiles and replays green through the contract alone.
Four falsification specs named fragments this rename moved and were retargeted
at the code that carries the same guard today; falsify:lint finds all 865
mutations again.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#514)

Two passages claimed what #514 measured to be false. docs/architecture.md: "a
pack receives no machine.Driver value at all … so the call it would have
written does not compile". docs/fourth-pack.md: "it could not name
machine.Driver if it tried: since #511 emulator.Env hands out no driver value".

Both were true of *obtaining* a driver and neither was true of *naming* one. On
154c204, `var _ machine.Driver` in internal/providers/scaleway compiled and
`go build ./internal/providers/scaleway/` exited 0. That is the shape this
repository calls a comment standing in for a control, and documentation is
where it survives longest, because nothing runs it.

They now say which step closed which half, and name the test that compiles the
forbidden sentence.

docs/limits.md loses two references to types that are unexported since: the
driver's Detach and the runtime's balancing half. The passage on #475 keeps
`machine.Firewaller` — it is explicitly "until #475 was fixed", and rewriting a
dated record to match today's spelling would make it say something that was not
what was measured.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rator types it

The parameter was called alias while every caller passes the bare
<family>/<version> the CLI reads, and the driver is what prepends the
emulator's own prefix. A name that asserts a shape the value does not have is
the smallest version of the defect this lot is about.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@stephrobert
stephrobert merged commit 2879888 into main Aug 28, 2026
30 checks passed
@stephrobert
stephrobert deleted the feat/514-driver-unnameable branch August 28, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant