@@ -48,6 +48,32 @@ namespace as user data.
4848 This drives the historical-scan cost we measured at ~ 17us/frame ×
4949 110k frames per dispatcher start.
5050
51+ ### Concrete deficiencies enumerated
52+
53+ The problems above surface as eight specific bugs in today's
54+ implementation. Each is referenced by the invariant set below.
55+
56+ 1 . Service ` .stopped {reason=finished} ` and ` .stopped {reason=error} `
57+ restart on boot. Contrary to "if it chose to stop, it stays stopped."
58+ 2 . Service hot-replace where the new ` .spawn ` has a parse error: old
59+ keeps running live (correct), but compaction is latest-wins so the
60+ broken ` .spawn ` survives and the service vanishes on next boot.
61+ 3 . Action hot-replace + parse error: same shape as #2 -- broken ` .define `
62+ overwrites; previously-good define is lost on restart.
63+ 4 . Action with a broken ` .define ` retries the broken version on every
64+ boot. Dispatcher doesn't scan ` .error ` historically; no "skip broken"
65+ path.
66+ 5 . Actor hot-replace + parse error kills * both* : the old instance
67+ self-terminates on any duplicate ` .register ` before the new is
68+ validated; the new then parse-fails. Net: nothing running.
69+ 6 . Action has no undefine. Once defined, the only way to remove is to
70+ edit history.
71+ 7 . Action ` .error ` overloads parse failure (` register_action ` Err) and
72+ runtime failure (per-call ` execute_action ` Err) -- can't tell apart
73+ at the topic level.
74+ 8 . Actor ` .unregistered ` overloads parse failure with graceful
75+ teardown -- only ` meta.error ` 's presence distinguishes them.
76+
5177## Decision
5278
5379### Namespace
@@ -163,6 +189,60 @@ if xs died before the `fin.term` ack landed. Acceptable -- a `term` in
163189the log is a clear user intent, and respecting it without waiting for
164190the ack matches what the user wanted.
165191
192+ ### Invariants
193+
194+ The compaction algorithm and topic vocabulary above exist to honor these
195+ contracts. Each one is testable; together they cover every deficiency in
196+ the previous section.
197+
198+ - ** I1. Stop persistence.** Once ` term ` or any ` fin.* ` has been observed
199+ for a ` <kind>.<name> ` , no subsequent restart starts the prior ` create ` .
200+ - ** I2. Run persistence.** A ` <kind>.<name> ` with an ` active ` and no
201+ subsequent ` fin.* ` /` term ` resumes on every restart until something
202+ terminal lands.
203+ - ** I3. Hot-replace fallback.** When a newer ` create₂ ` follows a
204+ known-good ` create₁ ` , and ` create₂ ` is broken (` parse.error ` ) or
205+ untested (no ack), restarts fall back to ` create₁ ` . Live behaviour
206+ and post-restart behaviour agree.
207+ - ** I4. Bidirectional lifecycle.** Every kind supports a user-driven
208+ ` term ` that ends the thing and prevents restart.
209+ - ** I5. Distinct exit categories.** The topic alone (no meta needed)
210+ distinguishes: failed-to-init vs user-terminated vs runtime-crashed
211+ vs naturally-finished vs replaced vs server-shut-down.
212+ - ** I6. Ack traceability.** Every runtime-emitted ack (` active ` ,
213+ ` parse.error ` , ` fin.* ` , ` replaced ` ) carries a meta pointer to its
214+ originating ` create ` (or ` term ` ).
215+ - ** I7. Server-shutdown invisibility.** A ` stopped ` event does not
216+ affect compaction; the thing resumes on next start.
217+ - ** I8. Single live instance.** At most one running instance per
218+ ` <kind>.<name> ` at any time.
219+
220+ ### Coverage check
221+
222+ Each enumerated deficiency would be caught by a test of the named
223+ invariant:
224+
225+ | # | Deficiency | Caught by |
226+ | ---| ---| ---|
227+ | 1 | Service ` .stopped {finished/error} ` restarts on boot | I1 |
228+ | 2 | Service hot-replace + parse error: old version lost on restart | I3 |
229+ | 3 | Action hot-replace + parse error: same | I3 |
230+ | 4 | Action broken ` .define ` retries every boot | I3 |
231+ | 5 | Actor hot-replace + parse error kills both | I3 |
232+ | 6 | Action has no undefine | I4 |
233+ | 7 | Action ` .error ` overloads parse and runtime failure | I5 |
234+ | 8 | Actor ` .unregistered ` overloads parse-failure with graceful teardown | I5 |
235+
236+ I6 and I8 don't catch one of the enumerated deficiencies directly, but
237+ they're load-bearing for the invariants that do: without ack traceability
238+ (I6) you can't pair ` parse.error ` to its ` create ` , so I3 is unenforceable;
239+ without single-instance (I8) you can't unambiguously define "the thing"
240+ that I1/I2 track. They stay in the set as supporting invariants.
241+
242+ I7 is implied by I2 + I1 (` stopped ` isn't ` fin.* ` so it doesn't satisfy
243+ I1's "stop observed"), but stating it explicitly closes a likely
244+ misreading of ` stopped ` as a terminal event.
245+
166246### Action subset
167247
168248Actions don't run long-lived tasks. The events they use:
0 commit comments