Skip to content

Commit f7d8e9d

Browse files
committed
docs(adr-0087): point the pre-addendum retirement claim at its correction, and record the artifact-ingestion door
Two records-only corrections to ADR-0087, both additive — no sentence is rewritten or deleted, because an accepted ADR's text binds and a superseded decision is still a record (Prime Directive #13). 1. The "retired entry is skipped by the loader" paragraph under "The load-window's second half is now mechanical" states the authoring load path's rule and stands 35 lines above the 2026-07-31 addendum that supersedes it for metadata at rest. It gains an in-place blockquote pointer naming both addenda, in the same form ADR-0044 and ADR-0058 use. 2. The artifact-ingestion door's policy was ruled on #12772 and recorded in no addendum: the ADR's reader met the stored-row policy and nothing about the boot seam. A ninth addendum records it in the file's own addendum form, quoting the ruling verbatim, with the as-built window, the two in-tree doors, the default-flip exclusion, the schema-layer residue boundary and the flag's live seam inventory. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 65767d2 commit f7d8e9d

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

docs/adr/0087-metadata-protocol-upgrade-contract.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,17 @@ in N+1 — but never deleted": a retired entry is skipped by the loader
358358
fixture CI. Live-window entries (currently the protocol-15 ADR-0089 visibility
359359
aliases) stay load-active until they graduate.
360360

361+
> **Superseded for metadata at rest** (2026-07-31, #3903; 2026-09-13, #12772).
362+
> The paragraph above states the rule for the **authoring** load path and for
363+
> nothing else, and a reader who stops here carries it to seams where it is
364+
> false. Retirement is an authoring-surface event: the loader passes a retired
365+
> entry over so a live author is taught the canonical spelling. Metadata at rest
366+
> has no author to teach, so it replays the full chain, retired entries
367+
> included — `sys_metadata` rows unconditionally (the 2026-07-31 addendum
368+
> below), and a compiled artifact inside the versioned window its own manifest
369+
> opens (the 2026-09-13 addendum below). Kept in place rather than rewritten,
370+
> because a superseded decision is still a record (Prime Directive #13).
371+
361372
### Ratified: the pre-launch launch-window exemption (majors 12–15)
362373

363374
Majors 12–14 shipped breaks as **pre-launch one-step changes with no alias
@@ -825,3 +836,93 @@ their PR touched.
825836
lands, per the ruling, and one of them (`declare-search-clone-response-contracts`)
826837
fails predicates 2 and 3 outright — it bumps `@objectstack/spec` and adds
827838
`packages/spec/src/api/protocol.zod.ts`, so it is not a member of this class.
839+
840+
## Addendum (2026-09-13) — the artifact-ingestion door opens a versioned window (#12772)
841+
842+
The addenda above reach authored source and metadata at rest in `sys_metadata`.
843+
A third seam was reached by neither: a **compiled artifact**
844+
(`objectstack build``dist/objectstack.json`) is data at rest *with a version
845+
stamp*, and nothing replayed the chain before its strict parse. Measured on
846+
#12772: an app built by its pinned released 17.1.0 toolchain carried **75**
847+
occurrences of `allowPurge` / `allowRestore` that its own source never wrote —
848+
the released builder injected the then-legal permission bits — and booting that
849+
artifact on a 17.2.0 runtime was refused at the door by the #12497 tombstone.
850+
`os migrate meta` could not help: it targets sources, not built artifacts, so
851+
the only operator remedy found was hand-editing the artifact JSON. The policy
852+
that closed it shipped inside the v17 line; it lived in the issue and in no
853+
addendum here, which is the omission this entry corrects (#17894).
854+
855+
**The ruling** (#12772 triage, comment `5443380108`, 2026-08-27), verbatim:
856+
857+
> So the conversion must be a *versioned forward conversion keyed off the
858+
> artifact's authored `specVersion`*, ⛔ not an unconditional strip — an
859+
> unconditional strip becomes wrong again the moment M2 lands and the keys are
860+
> legal once more.
861+
862+
The M2 half is the load-bearing reason and not a stylistic one: #12497 retired
863+
these keys with the M2 anchor left open (#1883, `pm:on-hold`), so an
864+
unconditional strip would start deleting legal metadata the day the keys return.
865+
866+
- **The window, and what keys it.** The policy is one function —
867+
`packages/metadata-core/src/artifact-forward-conversion.ts#applyArtifactForwardConversions`
868+
— and it compares two versions: the **floor** of the range the artifact's
869+
manifest declares (`engines.protocol`, ADR-0025) and the `@objectstack/spec`
870+
version the process actually runs. `floor < runtime` replays the FULL chain,
871+
retired entries included, before the strict parse — the artifact is the
872+
"consumer arriving late" D3 keeps every conversion forever for. `floor >=
873+
runtime` replays nothing: the artifact claims the current or a newer surface,
874+
and the strict parse, tombstones included, stays the authority. That branch is
875+
what makes the window *versioned rather than a blanket amnesty*, and it is the
876+
branch the M2 return needs. No declared range replays (an artifact of unknown
877+
age is old data at rest, and conversions only rewrite shapes they positively
878+
recognize); an unresolvable runtime version replays nothing, because amnesty
879+
rests on positive version evidence.
880+
- **⚠️ The shipped key is the DECLARED FLOOR, not the authored version.** The
881+
ruling says "authored `specVersion`"; what an artifact manifest actually
882+
carries is a protocol *range*, so the implementation keys off that range's
883+
floor. The difference is not cosmetic — `^17.0.0` is the range
884+
`create-objectstack` stamps, so an app authored today lands inside the window.
885+
Recorded here as the as-built reading, because the next two bullets are its
886+
consequences.
887+
- **One policy, not per-door copies** (#12772, the card's own shape). Two
888+
in-tree doors consume that one function: the metadata plugin's artifact
889+
ingestion (`packages/metadata/src/plugin.ts`) and the runtime app plugin's
890+
security bundle (`packages/runtime/src/app-plugin.ts`). ⛔ A door that
891+
re-implements the comparison is the drift this bullet exists to forbid.
892+
- **⛔ The window does NOT admit default flips** (narrowed later, on #16693 and
893+
#17885). A retirement whose old shape has no live meaning left is safe to
894+
replay here. A DEFAULT FLIP is not: its old shape still parses, still means
895+
something on today's authoring surface, and rewriting it is a
896+
reinterpretation — sound only where "this input predates the flip" is a fact,
897+
and at this door it is a guess, because the key is a declared floor and not an
898+
age. The exclusion list is
899+
`packages/metadata-core/src/artifact-forward-conversion.ts#DEFAULT_FLIPS_NOT_REPLAYED_HERE`;
900+
an entry on it keeps firing at the stored-row seams and under
901+
`os migrate meta`, where the old meaning is the only meaning.
902+
- **Where the window starts, relative to the schema.** A retired key carrying
903+
only its *emitted default* parses as inert residue and is stripped silently at
904+
the SCHEMA layer (the `acceptRetiredDefaultResidue` stage, ruled on #12845 and
905+
recorded in this card's review, comments `5448522858` / `5448958115`); the
906+
door's window is not what rescues that class. What the window admits is the
907+
class the schema still refuses: a retired key carrying a NON-default value,
908+
which keeps the #12497 tombstone at the current version and is convertible
909+
only because the artifact says it was authored before the retirement.
910+
- **The flag's live inventory**, measured for this entry: three
911+
`includeRetired: true` literals in runtime source — the artifact policy above,
912+
the automation engine's stored-flow canonicalization seam
913+
(`packages/services/service-automation/src/engine.ts#canonicalizeStoredFlow`,
914+
which `registerFlow` calls), and
915+
`packages/spec/src/conversions/stored.ts#applyConversionsToStoredItem` — and
916+
**four** runtime callers, because the artifact policy is called by two doors.
917+
`stored.ts` **pins** the flag rather than offering it
918+
(`packages/spec/src/conversions/stored.ts#StoredConversionOptions` is an
919+
`Omit` over `includeRetired`), so no caller can turn the stored-row replay
920+
off.
921+
922+
**What this does not decide.** The cloud composed-artifact door
923+
(`OS_COMPOSED_ARTIFACT_URL`) is a second door in another repository and was
924+
recorded, not filed, by the #12772 triage: it needs the same treatment and
925+
should CONSUME this policy rather than reimplement it. Nothing here changes the
926+
protocol handshake's jurisdiction either — a cross-major gap refuses at
927+
`checkProtocolCompat` before conversion could matter, and this window is for
928+
within-line retirements (17.1 → 17.2), which are the case that created it.

0 commit comments

Comments
 (0)