Skip to content

Commit 01ccbf2

Browse files
ausimianclaude
andcommitted
docs: document the command boundary and spec its public surface
The @moduledoc was still the placeholder `mix new` writes and none of the commands carried a @doc or a @SPEC, so the published page said nothing about the one thing a reader has to know before calling any of it: these are not an Elixir API. Every function but `customize/1` is a command entry point that `bin/castle` reaches over `rpc` - it prints its report and returns a bare `:ok`, and it raises `Castle.Error` on failure so that the calling VM exits non-zero. A reader who takes them for ordinary functions is surprised by both halves, so the moduledoc says what a command boundary is and every @doc names the `bin/castle` command that reaches it. castle#11. The distinction the issue asked to be settled, settled deliberately: * `make_releases/0` is @doc false. Its only caller is the launcher's `env.sh` fragment, in the preboot VM of a start whose deployment has no `RELEASES` yet, and by hand it either does nothing or does what the next start would do anyway. It keeps its @SPEC: the spec is the contract whether or not the function is published. * `install/2..5` is documented as what it is. One @doc covers every arity of a clause with defaults, so silence about the extra four would leave a test seam reading as an API. * Everything else is a command an operator invokes, and is documented with that framing rather than hidden - `upgradable/0` included, since the state it reports is invisible otherwise and a diagnostic nobody is told about is one nobody thinks to ask. The specs say `:: :ok` and nothing more, because that is the whole of what `report!/1` returns. `mix docs` is clean; its one complaint was a reference to `Kernel.CLI`, which is hidden, so `Castle.Error`'s own wording is used instead. Three internal notes had gone stale and are corrected with it: two named a `commit/3` that is now `commit/5`, and one claimed `commit` still composes materialisation outside the serialised region, which castle#14 ended. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4oaMvbR1cbxrZBj8qwkqN
1 parent 0564785 commit 01ccbf2

4 files changed

Lines changed: 349 additions & 22 deletions

File tree

AGENTS.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,11 @@ Castle's job is configuration and release management on a running node.
515515
more: a diagnostic, not a gate, and nothing has to call it. It stays because
516516
the state it reports is otherwise invisible — the file can be present while the
517517
record the node works from was synthesised — so an operator needs a way to ask
518-
that does not unpack or install anything. Whether it belongs in the documented
519-
API surface is [#11](https://github.com/ausimian/castle/issues/11)'s to settle.
518+
that does not unpack or install anything.
519+
[#11](https://github.com/ausimian/castle/issues/11) settled that it belongs in
520+
the documented surface, and for that same reason: `bin/castle upgradable` is
521+
how an operator asks, and a diagnostic nobody is told about is one nobody
522+
thinks to ask.
520523
- **`unpack/1`, `install/1`, `commit/1`, `remove/1`, `releases/0`** — wrappers
521524
over `:release_handler`, with the target version's configuration materialised
522525
before `install` and `commit` hand it over, the record check inside `unpack`
@@ -925,6 +928,34 @@ rather than reporting an outcome. See **Release integration** below. Anything
925928
that says "every function in `Castle`" has to say "but `customize/1`", and the
926929
comment at the head of `lib/castle.ex` does.
927930

931+
**What is published and what is hidden follows from that, and is
932+
[#11](https://github.com/ausimian/castle/issues/11)'s decision.** The whole of
933+
`Castle` now carries `@doc` and `@spec`, and the `@moduledoc` says the two
934+
things a reader has to know before calling any of it: that this is the runtime
935+
half of a pair, and that these are commands rather than an API — a command
936+
prints its report and returns `:ok`, so the return value carries nothing, and a
937+
failure raises `Castle.Error` rather than returning `{:error, _}`. Every command
938+
`@doc` names the `bin/castle` command that reaches it, because that is the
939+
interface and the function is the thing behind it.
940+
941+
Two decisions inside that. `make_releases/0` is `@doc false`: its only caller is
942+
the launcher's `env.sh` fragment, in the preboot VM of a `start` or `daemon`
943+
whose deployment has no `RELEASES` yet, and by hand it either does nothing (the
944+
file is there) or does what the next start would do anyway. Its contract is with
945+
a shell fragment in another project, so publishing it would document a function
946+
nobody should call. It keeps its `@spec` regardless — the spec is the contract
947+
whether or not the function is published. And `install/2..5` is documented as
948+
what it is, a seam the concurrency test drives: one `@doc` covers every arity of
949+
a clause with defaults, so saying nothing about the extra four would leave them
950+
reading as an API. Every other function is a command an operator invokes, and
951+
hiding one of those would document nothing useful anywhere.
952+
953+
The specs say `:: :ok` and nothing more, because that is what `report!/1`
954+
returns; a spec naming the lines, or an error tuple, would be describing
955+
`Castle.Commands`. Nothing checks them — there is no Dialyzer here — so they are
956+
kept by hand, and a claim in a `@doc` about what a command refuses is worth
957+
checking against `Castle.Commands` before it is trusted.
958+
928959
Forecastle is what arranges for these to be reachable: it leaves the
929960
configuration Mix wrote alone, adds a `:preboot` script that starts `:castle`,
930961
and writes the `env.sh` fragment and `bin/castle` wrapper that call into this
@@ -1418,13 +1449,16 @@ wrote, so Elixir's pipeline is still armed in the file the launcher reads.
14181449
it. It is the one limitation here that a lock cannot narrow, which is why the
14191450
filesystem half of the protocol — `publish/2` refusing rather than replacing —
14201451
has to stand on its own.
1421-
- **The public API is undocumented, apart from `customize/1`.** `@moduledoc` is
1422-
still the generated placeholder and the commands carry no `@doc` or `@spec`
1423-
annotations ([#11](https://github.com/ausimian/castle/issues/11)).
1424-
`Castle.customize/1` has both, and was documented with #12 rather than left
1425-
for #11 because it is the function a consumer's `mix.exs` calls and nothing
1426-
else says how. It is the standard the rest has to be brought up to, not an
1427-
exception to be levelled down.
1452+
- **Nothing checks the `@spec`s.** The public surface is documented as of
1453+
[#11](https://github.com/ausimian/castle/issues/11) — see the end of *What it
1454+
does* for what is published and why — but there is no Dialyzer in this
1455+
project, so a spec that stops describing its function fails nothing. They are
1456+
all `:: :ok` today, which is the whole of what `report!/1` returns, so the way
1457+
one goes wrong is a command that starts returning something else and a spec
1458+
that keeps saying `:ok`. The same holds for what the `@doc`s claim a command
1459+
refuses: `mix docs` catches a broken *reference*, and nothing at all catches a
1460+
true sentence that has stopped being true. Both are read against
1461+
`Castle.Commands` by hand.
14281462
- **The README is out of date.** It documents an `:appup` compiler and a
14291463
`mix castle.relup` task that moved to Forecastle in 0.3.0, the release
14301464
management commands it describes on `bin/<release>` now live on `bin/castle`,

RELEASE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,33 @@
260260
into a temporary hybrid release whose version directory holds a boot script and
261261
a configuration and none of the launcher's own files, so there is nothing for a
262262
launcher to boot; Forecastle refuses to generate one.
263+
- Documentation for the `Castle` module, which used to be the placeholder
264+
`mix new` writes. It now says what Castle is - the runtime half of a pair whose
265+
build-time half is Forecastle - and, before anything else, what kind of thing
266+
the functions in it are: `customize/1` is the build-time integration point and
267+
the only one meant to be called from other Elixir code, and everything else is
268+
a command entry point that `bin/castle` reaches over `rpc`. That distinction is
269+
worth reading before calling any of them, because a command prints its report
270+
and returns a bare `:ok`, and a command that fails raises `Castle.Error`
271+
instead of returning an error - which is what leaves a non-zero exit status
272+
behind for the shell, and is not what a caller expecting an ordinary function
273+
would write code for.
274+
275+
Every command is documented with the `bin/castle` command that reaches it -
276+
`releases`, `upgradable`, `unpack`, `install`, `commit`, `remove` - and with
277+
what it refuses and what it leaves behind: that an install is provisional until
278+
it is committed and a restart until then returns to the previous version, that
279+
`bin/castle install` confirms the version is running rather than trusting the
280+
reply, that removing a version deletes what nothing else is using, and that the
281+
two questions - `upgradable` and `releases` - answer on a deployment where
282+
everything else is refused. `Castle.running/1` is documented too: it has no
283+
`bin/castle` command of its own, and it is what automation driving an upgrade
284+
over `rpc` needs in order to confirm one.
285+
286+
Every public function now carries a `@spec`. `Castle.make_releases/0` is
287+
deliberately not published: its only caller is the launcher, on the first start
288+
of a deployment, and by hand it either does nothing or does what the next start
289+
would do anyway.
263290

264291
### Changed
265292

0 commit comments

Comments
 (0)