Found while running the ADR-0049 enforce-or-remove ledger determination dispatched on #16864 (that round is a determination only and writes no behaviour change, so this is filed separately rather than widened into it). Measured on origin/main e7fea4665d, runtime @objectstack/spec 17.4.0.
#16864 lists, as the first thing a fix would need, an audit nobody had run: "Whether any of the remaining 73 entries is a default flip rather than a lossless delete." This is that audit's positive hit, and it is reachable today.
The entry, and what its own docblock claims
packages/spec/src/conversions/registry.ts, the app-hidden-to-unpublished entry (#4829, ADR-0045 amended 2026-08-09), quoted verbatim from its docblock:
retiredFromLoadPath: true — load-bearing here, not bookkeeping.
Retirement is what confines this rewrite to stored rows. hidden is NOT
retired as an authorable key — it keeps its birth contract, narrowed to
navigation — so a conversion running on the load path would rewrite
defineApp({ hidden: true }), and ACCOUNT_APP itself, into unpublished apps
and reproduce #4829 through the conversion layer. Excluded from the load path,
it replays only where the old meaning is the only meaning: the stored-row
rehydration seams (applyConversionsToStoredItem, which pins
includeRetired) and os migrate meta.
packages/spec/src/ui/app.zod.ts backs the premise: hidden is a live authorable key — "Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead (navigation only — never an access gate)".
The measurement: the load path the docblock excludes includes the artifact-ingestion door
applyArtifactForwardConversions (packages/metadata-core/src/artifact-forward-conversion.ts:291, #12772) replays the full chain with includeRetired: true whenever an artifact's declared engines.protocol floor is below the running spec. It is the framework artifact door's policy, called from packages/metadata/src/plugin.ts:763 (MetadataPlugin._convertArtifactForward), which converts and then hands the result to ObjectStackDefinitionSchema.parse and registers it.
Probe, run against built dist/ on e7fea4665d. Subject: an artifact whose manifest declares engines.protocol: ^17.0.0 — the range create-objectstack stamps, measured on the registry canary in #16693 — carrying one app authored hidden: true.
runtime @objectstack/spec version = 17.4.0
[SUBJECT floor ^17.0.0] verdict = converted-forward
[SUBJECT floor ^17.0.0] notices = ["app-hidden-to-unpublished"] <- a RETIRED entry
[SUBJECT floor ^17.0.0] apps[0].hidden = undefined <- and it was APPLIED
[SUBJECT floor ^17.0.0] apps[0]._unpublished = true
[NEG CTRL floor ^99.0.0] verdict = authored-current <- window shut
[NEG CTRL floor ^99.0.0] notices = []
[NEG CTRL floor ^99.0.0] apps[0].hidden = true
[POS CTRL other conv ] notices contain action-inert-keys-removed = true <- window open, different entry
[POS CTRL other conv ] actions[0].shortcut (inert key) = undefined
[applyConversions includeRetired=false] notices=[] apps[0].hidden=true <- the flag WORKS when not overridden
[applyConversions includeRetired=true ] notices=["app-hidden-to-unpublished"] apps[0].hidden=undefined _unpublished=true
[normalizeStackInput (authoring seam)] notices=[] apps[0].hidden=true <- the seam the flag does govern
Door output carried through the strict parse the door itself feeds, which is what actually reaches registration:
[floor ^17.0.0] parse ok=true -> registered app: hidden=undefined _unpublished=true
[floor ^99.0.0] parse ok=true -> registered app: hidden=true _unpublished=undefined
The negative control proves the instrument can answer "no"; the positive control proves the window is open independently of the entry under test; the includeRetired=false leg proves the flag itself is not broken — it is overridden by the caller.
Why the rewritten value is not cosmetic
packages/rest/src/rest-server.ts:3215 is the consumer:
if (item._unpublished === true && !sysPerms.has('studio.access') && !sysPerms.has('setup.access')) {
The app is dropped for every user without studio.access / setup.access. So an app deliberately authored hidden: true (navigation presentation) is served as withheld-from-everyone — exactly the #4829 failure the _unpublished split was introduced to end, arriving through the conversion layer, which is the outcome the entry's docblock says retiredFromLoadPath prevents.
The trigger is a declared floor, not artifact age. Any app on a permissive range is treated as a pre-split source regardless of when it was authored — the same shape #16693 measured for field-required-notnull-explicit before that entry was removed.
What is NOT measured here
- ⛔ No end-to-end boot of a real scaffolded app was run. The measurement is the door policy function plus the strict parse it feeds, plus the consumer read at
rest-server.ts:3215 — read, not executed.
- ⛔ Whether any deployed app authors
hidden: true is not measured. The platform's own account app does, but it is code-declared (platform-objects ACCOUNT_APP) and, per the entry's docblock, code-declared artifacts do not enter sys_metadata — whether they enter a compiled artifact through this door is a separate reading nobody has taken.
- ⛔ The remaining 11 retired entries whose old shape the current schema still accepts are page-component keys on a permissive surface; they were not individually classified as flip-vs-lossless. 19 of the 75 retired entries could not be classified at all (their fixtures do not parse standalone) and read NOT MEASURED.
Not the same card as #16864
#16864 is the ledger question — does retiredFromLoadPath mean what it says, and which of the flag or the seams gives. This card is one measured consequence with a named subject, a named consumer and a reproducible probe, and it stands whichever way that ledger question is answered. Dedup: full enumeration of all 534 open non-PR issues (six REST pages, number line closed) greps zero other card mentioning app-hidden-to-unpublished, _unpublished, or applyArtifactForwardConversions; the lit control (retiredFromLoadPath) returns #16864, so the grep works.
Refs #16864, #12772, #16693, #4829, ADR-0045, ADR-0087 D2 addendum 2026-07-31.
Generated by Claude Code
Found while running the ADR-0049 enforce-or-remove ledger determination dispatched on #16864 (that round is a determination only and writes no behaviour change, so this is filed separately rather than widened into it). Measured on
origin/maine7fea4665d, runtime@objectstack/spec17.4.0.#16864 lists, as the first thing a fix would need, an audit nobody had run: "Whether any of the remaining 73 entries is a default flip rather than a lossless delete." This is that audit's positive hit, and it is reachable today.
The entry, and what its own docblock claims
packages/spec/src/conversions/registry.ts, theapp-hidden-to-unpublishedentry (#4829, ADR-0045 amended 2026-08-09), quoted verbatim from its docblock:packages/spec/src/ui/app.zod.tsbacks the premise:hiddenis a live authorable key — "Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead (navigation only — never an access gate)".The measurement: the load path the docblock excludes includes the artifact-ingestion door
applyArtifactForwardConversions(packages/metadata-core/src/artifact-forward-conversion.ts:291, #12772) replays the full chain withincludeRetired: truewhenever an artifact's declaredengines.protocolfloor is below the running spec. It is the framework artifact door's policy, called frompackages/metadata/src/plugin.ts:763(MetadataPlugin._convertArtifactForward), which converts and then hands the result toObjectStackDefinitionSchema.parseand registers it.Probe, run against built
dist/one7fea4665d. Subject: an artifact whose manifest declaresengines.protocol: ^17.0.0— the rangecreate-objectstackstamps, measured on the registry canary in #16693 — carrying one app authoredhidden: true.Door output carried through the strict parse the door itself feeds, which is what actually reaches registration:
The negative control proves the instrument can answer "no"; the positive control proves the window is open independently of the entry under test; the
includeRetired=falseleg proves the flag itself is not broken — it is overridden by the caller.Why the rewritten value is not cosmetic
packages/rest/src/rest-server.ts:3215is the consumer:The app is dropped for every user without
studio.access/setup.access. So an app deliberately authoredhidden: true(navigation presentation) is served as withheld-from-everyone — exactly the #4829 failure the_unpublishedsplit was introduced to end, arriving through the conversion layer, which is the outcome the entry's docblock saysretiredFromLoadPathprevents.The trigger is a declared floor, not artifact age. Any app on a permissive range is treated as a pre-split source regardless of when it was authored — the same shape #16693 measured for
field-required-notnull-explicitbefore that entry was removed.What is NOT measured here
rest-server.ts:3215— read, not executed.hidden: trueis not measured. The platform's ownaccountapp does, but it is code-declared (platform-objectsACCOUNT_APP) and, per the entry's docblock, code-declared artifacts do not entersys_metadata— whether they enter a compiled artifact through this door is a separate reading nobody has taken.Not the same card as #16864
#16864 is the ledger question — does
retiredFromLoadPathmean what it says, and which of the flag or the seams gives. This card is one measured consequence with a named subject, a named consumer and a reproducible probe, and it stands whichever way that ledger question is answered. Dedup: full enumeration of all 534 open non-PR issues (six REST pages, number line closed) greps zero other card mentioningapp-hidden-to-unpublished,_unpublished, orapplyArtifactForwardConversions; the lit control (retiredFromLoadPath) returns #16864, so the grep works.Refs #16864, #12772, #16693, #4829, ADR-0045, ADR-0087 D2 addendum 2026-07-31.
Generated by Claude Code