Declare for collections: adding one should not mean stopping the server
Shipped as frame 14, establish — not as an extension of frame 13,
which is what this ticket proposed. The reason is the freeze rule this same backlog writes down: frame
13 is already Declare and already accepts exactly one payload shape,
{"operation": …}. Widening it would have changed what an existing frame accepts, which is
precisely the thing SAPE-2 says the tag forbids. New behaviour takes a new
frame; 14 was the next free number, and it is now spent.
The consequence is worth stating because the original text above argues the other way: "adding
collections later means either a second frame for a thing the first frame is already named after, or
changing what frame 13 accepts." The first horn is what happened. Frame 13 is
Declare and carries operations; frame 14 is establish and carries
collections. That naming is slightly awkward forever, and it was still the cheaper of the two.
sapedbd: declare-then-write on
one connection with no restart; the daemon's pid compared before and after, not
inferred from the socket staying up; other connections serving across the declaration; the
collection and its documents surviving a restart; an invalid spec refused with
apply's own validation and leaving nothing partial behind; the operator gate; the audit
entry.Reach, measured: the frame is exercised from the CLI, from all three clients, and from the desktop workbench — which gained a "New collection" form, an in-place upgrade path that says UPGRADES IT IN PLACE before the button is pressed, and a red banner naming an index the form would delete, also before the button is pressed. The upgrade case is measured the way that matters: three documents written before adding an index, then read back both by primary key and through the new index, proving the index was built over existing data rather than merely declared.
ISS-21: the refusal this frame raises most —
store.ErrIncompatible, "this does not match what was declared before" — is not in
codeFor, so it reaches a client as "failed". It was harmless while that error
could only happen offline inside apply, where a human reads the sentence. This frame gave
it a program as its audience.
Extend frame 13 so a collection can be declared against a running daemon, the way an operation already can. Same connection, same proof of the server's secret, same audit entry. Adding a collection stops being an outage.
pipelines/tasks/0071 named three shapes of external operation the user actually wants:
(1) one operation sapedb is missing, (2) several operations folded into one — function and
procedure, in the Pascal/Ada sense — and (3) one operation that solves a whole textbook
problem end to end, through the shapes it publishes. Measuring shape (3) is what changes this ticket's
rank: it is not one operation, it is a packaged business module, and a module carries
its own storage — collections, indexes, rollups — alongside the vocabulary (operations) and the typed
surface (shapes) built on top of it.
internal/server/declare.go is the frame-13 handler, and it is measured, not assumed: it
declares exactly one type, store.Operation
(declare.go:46, json:"operation"). A repository-wide search of that file for
Spec or Collection — the type and the field a collection declaration needs —
returns zero matches. So a KindDeclaration external operation (the shape
0071 recommends for v1: a signed bundle of declarations, no third-party code) can carry its
Operations over the wire today, but not the Collections they read and write.
It cannot install into a database that does not already have the collection by some other
means — and the only other means is stopping the daemon and running sapedb apply
by hand, which is exactly the outage this ticket exists to remove.
This is why SAPE-14 now sits ahead of, not beside, the tickets it blocks: SAPE-11 (listing installed external operations) and SAPE-12 (the worked external operation, published as a challenge) both assume an external operation can be installed on a database that did not already anticipate it. For the shape of external operation the user cares about most — a self-contained module, not a single missing verb — that assumption is false until this ships.
It is a public-surface asymmetry, and the surface is the one 1.0.0 freezes hardest. Frame 13 is
Declare — not DeclareOperation — and a client library's
Declare(operation) is the shape every client author is copying right now. Adding
collections later means either a second frame for a thing the first frame is already named after, or
changing what frame 13 accepts, and SAPE-2 freezes frame roles at the tag. The
cheap moment to make the frame whole is before anyone has written against half of it.
Measured 2026-09-20. Both halves exist in the store:internal/store/store.go:144hasfunc (s *Store) Declare(spec Spec) (*Collection, error)andinternal/store/ops.go:367hasDeclareOperation. Only one of them is reachable over the wire. Searching every non-test file for callers of.Declare(outside the store finds exactly one:internal/cli/cli.go:683— theapplypath, which works on the database file directly. Nothing ininternal/server/calls it, so the only way to add a collection is to stop the daemon, runapplyagainst the file, and start it again. The engine method has been there the whole time; what is missing is the frame handler in front of it.
Re-confirmed while measuring task 0071:internal/server/declare.godeclares adeclaringstruct with exactly one field,Operation store.Operation(declare.go:44-53). Grepping the file forSpecand forCollectioneach return nothing.
apply, and refusing
leaves no partial collection behind: after the refusal, the catalogue does not list it.Explore and operation-declare are.sapedb apply
step. This is the criterion 0071 added; the seven above are what SAPE-19 already proved for
operations. Not met — carried to SAPE-12, see above.apply path. It stays; it is what works on a file nobody is serving.