@@ -30,7 +30,9 @@ not by a subscriber a new write path might forget to trigger:
3030 ` publishMetaItem ` / ` deleteMetaItem ` , after persistence and before the write returns.
3131 The projector is the ** only writer** of the record. A Studio save therefore returns
3232 only after the record already reflects it — no projection race (the #2867 subscriber
33- was fire-and-forget).
33+ was fire-and-forget). (** The three doors named above are no longer the whole list** —
34+ the recovery doors project too since 2026-09-03; the sentence is left as written and
35+ the full enumeration is in the 2026-09-04 amendment at the end of this record.)
34363 . ** Boot reconciliation + one-time backfill.** At ` kernel:ready ` the projection is
3537 re-derived from metadata (metadata wins), and legacy records that exist ** only** in
3638 the data plane are migrated into the metadata store once.
@@ -91,7 +93,7 @@ recreates ids.
9193** Assignments and bindings stay data-plane.** Which users/positions hold a set is
9294environment * state* , not part of the definition; those tables are unchanged.
9395
94- ### D2 — The record is written only by the projector, awaited by the protocol
96+ ### D2 — The record is written only by the projector, awaited by the protocol (door enumeration amended 2026-09-04 — see the amendment at the end)
9597
9698` @objectstack/metadata-protocol ` gains ` registerMutationProjector(type, fn) ` : an
9799awaited, best-effort per-type hook invoked after persistence inside ` saveMetaItem `
@@ -100,6 +102,14 @@ awaited, best-effort per-type hook invoked after persistence inside `saveMetaIte
100102write's response (` projectionApplied: { success:false, error } ` ) and logged — never
101103thrown, the metadata write itself succeeded and boot reconciliation heals on next start.
102104
105+ > ** Amended 2026-09-04** (see the amendment at the end of this record): the two
106+ > sentences above are narrower than the shipped code in two ways. The door list is
107+ > three of ** seven** call sites, and boot reconciliation is not the only thing that
108+ > re-derives a record a failed projection left stale — every projecting door does,
109+ > and since 2026-09-03 that set includes the recovery doors. ` projectionApplied ` is
110+ > also not surfaced at every site: three of the seven call and await the projector
111+ > without putting an outcome on the response.
112+
103113` plugin-security ` registers the ` permission ` projector. It re-reads the ** fresh layered
104114effective body** and:
105115
@@ -484,3 +494,96 @@ two rules the `object` authoring gate (`objectPostureGate`,
484494Door order, as pinned in ` packages/rest/src/meta-object-owd-gate.test.ts ` :
485495the 422 lint gate answers first; this seam's 403 R1 door answers for writes
486496that pass lint.
497+
498+ ## Amendment (2026-09-04, #15244 ): the projector has seven doors, not three — and boot reconciliation was never the only re-derivation
499+
500+ D2 enumerates the doors that await the projector as ` saveMetaItem ` /
501+ ` publishMetaItem ` / ` deleteMetaItem ` . That sentence was accurate when this
502+ record was written and is ** incomplete** on today's tree: PR #14982 (merged
503+ 2026-09-03 as ` 95464ed65 ` ) extended the projection to the recovery doors, and
504+ the record did not follow it — that PR's documentation half was closed void, so
505+ the code shipped alone. This amendment closes the gap and ** changes no
506+ decision** : D1 stands untouched, and D2's rule — * the record is written only by
507+ the projector, and the projector is awaited before the write returns* — is
508+ exactly what the four new call sites implement. Only the enumeration was wrong,
509+ which is the harder kind of error to notice, because nothing it says is false.
510+
511+ ### The seven projecting doors, measured on ` main `
512+
513+ ` runMutationProjector ` is declared once in
514+ ` packages/metadata-protocol/src/protocol.ts ` and called from ** seven** sites in
515+ that same file — D2's three, plus the four #14982 added:
516+
517+ | Projecting door | ` state ` passed | Body handed over | Outcome on the response |
518+ | :-- | :-- | :-- | :-- |
519+ | ` saveMetaItem ` | ` draft ` \| ` active ` | the saved item | ` projectionApplied ` |
520+ | ` runPublishSideEffects ` , the phase-2 publish helper | ` active ` | the published body | ` projectionApplied ` |
521+ | ` deleteMetaItem ` , repository branch | ` deleted ` | none | ` projectionApplied ` |
522+ | ` rollbackMetaItem ` , after its registry write-through | ` active ` | the restored version's body | — |
523+ | ` revertCommit ` , restore limb | ` active ` | the pre-commit body | — |
524+ | ` revertCommit ` , soft-remove limb | ` deleted ` | none | — |
525+ | ` deleteMetaItem ` , legacy raw-engine exit | ` deleted ` | none | ` projectionApplied ` |
526+
527+ Three things D2's three-name sentence hides, each measured on the call sites
528+ rather than inferred from the method names:
529+
530+ - ** The publish door is a shared helper, not ` publishMetaItem ` .** The projecting
531+ call site lives in ` runPublishSideEffects ` , at that helper's own body level,
532+ and ` publishMetaItem ` is one of ** two** callers — ` publishPackageDrafts `
533+ reaches the same projection through the same helper. D2 named only the first,
534+ so the package-draft publish path has been projecting, undocumented, since
535+ before this amendment.
536+ - ** ` deleteMetaItem ` projects from two exits, not one.** Its repository branch
537+ always did; #14982 added the legacy raw-engine exit, which serves the
538+ code-only types the repository path cannot.
539+ - ** ` projectionApplied ` is not universal.** D2's failure-surfacing sentence
540+ holds at four of the seven. ` rollbackMetaItem ` and both ` revertCommit ` limbs
541+ declare no such key on their response types and #14982 deliberately added
542+ none: there the projector is called and awaited, and a failure is logged
543+ without reaching the caller.
544+
545+ The count is enforced, not incidental —
546+ ` packages/metadata-protocol/src/protocol.recovery-doors-mutation-projector.test.ts `
547+ pins "exactly seven" call sites, three original and four recovery-door, with the
548+ declaration asserted separately as a non-vacuity control. Anyone can re-derive
549+ the number:
550+
551+ ``` bash
552+ git grep -c " this.runMutationProjector(" -- packages/metadata-protocol/src/protocol.ts
553+ ```
554+
555+ which answers ` 7 ` on this tree. If it ever answers anything else, the table above
556+ is what went stale.
557+
558+ ### Boot reconciliation is not the only healing path
559+
560+ D2's best-effort clause ends "boot reconciliation heals on next start", which
561+ reads as though a failed projection is stranded until the next boot. It is not,
562+ and it never quite was: ** any** later projecting door on the same name re-derives
563+ the record, because the projector re-reads the fresh layered effective body
564+ rather than trusting whatever it was handed. Before #14982 that set was an
565+ unrelated save, publish or delete; since #14982 it also includes a rollback and
566+ both limbs of a commit revert. D4's ` kernel:ready ` pass remains the guaranteed
567+ floor — the one heal that needs no subsequent write, and the property D2's
568+ sentence was reaching for — but it is a floor, not the only path.
569+
570+ The inverse defect is the one #14982 fixed. Before it, the recovery doors
571+ restored the row and the in-memory registry without projecting, so a rollback
572+ left the derived record on the rolled-back-from state: applied everywhere except
573+ the record Setup reads, until an unrelated write or the next boot caught up.
574+
575+ ### This mechanism has one major left to live
576+
577+ [ ADR-0131] ( ./0131-total-organization-ownership-no-null-organization-id.md )
578+ (merged 2026-09-04 as PR #14976 ) retires it. Its D2 names "the
579+ ` sys_permission_set ` projector and reconciler of ADR-0094 D2/D4" among the
580+ machinery that goes, and its D3 retires the ` sys_permission_set ` object itself
581+ (with D13) — on the ground that ** D1 of this record was right and is being
582+ generalized** : the metadata layer was already the sole authoritative store, so
583+ once the catalog is read from the registry the projected row has nothing left to
584+ be. D14 puts that on the ** v18** line, and the card that executes it is C3,
585+ tracked on #15204 .
586+
587+ So this amendment describes a mechanism with one major to live. It is written
588+ for whoever has to keep the seven doors correct until then — not as an
589+ invitation to build something new on top of them.
0 commit comments