Commit 134b410
fix(metadata-core,spec): the artifact door stops replaying the default-flip class, so an authored
Fixes #17885
Clause-②: yes
The artifact-ingestion door replayed `app-hidden-to-unpublished` — a
DEFAULT FLIP — over authored metadata, so a compiled artifact carrying
`defineApp({ hidden: true })` reached registration as `_unpublished:
true` and was then withheld by `filterAppForUser` from every user
without `studio.access` / `setup.access`. The door now refuses that one
class by id. Nothing else about the door, the flag, the entry or the
consumer moves.
## Where the fix is, and why it is not anywhere else
| candidate | why not |
|---|---|
| `rest-server.ts:3215` (the consumer) | READ-ONLY in the declared
surface, and correct: withholding on `_unpublished` is the ADR-0045
gate. The defect is who WRITES `_unpublished`. |
| flip the door to `includeRetired: false` | Reverses #12772. The
retired window exists so an artifact built by 17.1.0 tooling carrying
`allowRestore`/`allowPurge` still boots instead of dying at the
tombstone. This branch's own suite proves it would break: the firing
control at `artifact-forward-conversion.test.ts` drives exactly that
artifact through the same window. |
| the registry entry's `apply` | The entry cannot answer the question. A
machine-written pre-split row and an author who wrote `hidden: true`
yesterday are byte-identical at the item level, so no predicate over the
app distinguishes them. Whether the rewrite is sound depends on the
CALLER's evidence, not on the item. |
| `retiredFromLoadPath` on the entry | Does not reach here at all.
#16864's determination landed on this base (PR #17888, commit
`29dd1a6ddb`): both recorded decisions bought the SEAMS arm, the flag is
`live` but its jurisdiction is the authoring funnel and nothing else.
`types.ts` now says so in terms: "For a conversion whose old and new
shapes are both legal and mean different things (a default flip, not a
rename), the data-at-rest seams will still apply it." |
⇒ The seam is where the evidence lives, so the seam is where the refusal
goes. `applyConversions` gains `excludeConversionIds` — "my evidence
cannot carry this entry" — and the door names the one class it refuses,
with the reason beside the id.
**Why THIS seam and not the others.** The other two data-at-rest seams
argue from "a row at rest has no author to teach". An artifact does have
one: it is compiled from a source that still exists, and the door's own
boot warning already tells that author to rebuild. Worse, the door's
evidence is the artifact's **declared `engines.protocol` floor**, not
its age — and `^17.0.0` is the range `create-objectstack` stamps, so an
app authored today lands inside the window. For a lossless delete or a
rename of a shape the schema now refuses, guessing wrong costs nothing:
the key is inert and the replay is a rescue. For a default flip,
guessing wrong destroys authored intent. Same measurement #16693 made
for `field-required-notnull-explicit`, and the WITHDRAWN block that
removal left in `registry.ts` says it in advance: "Before setting that
flag on a DEFAULT FLIP — as opposed to a lossless delete or a rename —
read that card, because the flag does not mean what its name and every
docblock around it say it means."
## ⛔ What is deliberately NOT settled here
- **The #16864 ledger question is not reopened.** `retiredFromLoadPath`
keeps exactly the jurisdiction that determination gave it. This adds a
seam-level refusal beside it; it does not redefine the flag.
- **`registry.ts` is untouched.** No entry is withdrawn, no `apply`
narrowed, no fixture moved. The entry still fires at the stored-row seam
and through `os migrate meta`, which is what its own docblock claims —
narrowing the door is what makes that claim TRUE again rather than
falsifying it further.
- **The consumer is untouched.** `packages/rest/src/rest-server.ts` is
not in the diff.
## The pin: subject, two controls, and the post-parse leg
`packages/metadata-core/src/artifact-forward-conversion.test.ts` — six
cases, modelled on the #16693 block directly above them:
1. **SUBJECT** — floor `^17.0.0`, runtime `17.4.0`: `verdict ===
'converted-forward'` and `authoredFloor === '17.0.0'` (anti-vacuity: the
window really is open), `apps[0].hidden === true`, `_unpublished`
undefined, no `app-hidden-to-unpublished` notice, and copy-on-write
hands back the same reference.
2. **⭐⭐ POST-STRICT-PARSE** — the same door output fed to
`ObjectStackDefinitionSchema.parse`, which is what
`MetadataPlugin._parseAndRegisterArtifact` does at `plugin.ts:915`, and
the app read back OUT of the parsed object. This is the object that
reaches registration; a pin on the conversion's return value alone would
not catch a parse that re-introduced the key.
3. **NEGATIVE CONTROL** — floor `^99.0.0`: `verdict ===
'authored-current'`, zero notices. The instrument can answer "no" for
the other reason.
4. **FIRING CONTROL 1** — a NON-retired conversion
(`page-kind-jsx-to-html`) still fires in the subject's own window, on an
artifact that also carries the hidden app.
5. **FIRING CONTROL 2** — a RETIRED conversion still fires in that same
window (`allowRestore`/`allowPurge` stripped). This is the control this
particular fix could plausibly have broken, and it is what makes "#12772
is not reversed" a measurement rather than a claim.
6. **SEAM SCOPE** — `applyConversionsToStoredItem('app', …)` still
converts. A fix that had neutered the entry would go green on all five
legs above and silently strand the stored population.
Plus three cases on the primitive in
`packages/spec/src/conversions/conversions.test.ts`: the refusal, its
firing control (one id refused, the rest of the chain runs, in the same
call), and "absent or empty list changes nothing".
## Ablation — both halves, on-disk proof and hash-verified restore
Each leg: assert the file equals its HEAD blob · count the marker (must
be 1) · delete the line · re-count (must be 0) and assert the blob hash
MOVED · run · restore with `git checkout HEAD -- FILE` · assert the hash
is back and `git diff HEAD` is empty. A `trap` on EXIT/INT/TERM holds
the restore on the crash path, and every path is absolute.
**A — the door half** (`excludeConversionIds:
DEFAULT_FLIPS_NOT_REPLAYED_HERE,` removed). Marker 1 to 0, blob
`309b8444` to `f2e84cab`:
```
Test Files 1 failed | 15 passed (16)
Tests 3 failed | 275 passed (278)
FAIL leaves `hidden: true` alone on an artifact the retired window IS open for
AssertionError: the authored navigation choice is untouched: expected undefined to be true
FAIL registers `hidden: true` — asserted AFTER the strict parse the door feeds
AssertionError: what registration receives: expected undefined to be true
FAIL still applies a non-retired conversion in that same window (its app-side line)
```
⭐ The direction is not just "red": the SUBJECT and the POST-PARSE legs
fail, while the NEGATIVE control, FIRING CONTROL 2 and the SEAM-SCOPE
leg stay green — exactly the three that must not depend on the fix.
Restore: hash back to `309b8444`, `git diff HEAD` empty.
**B — the primitive half** (`if (excluded?.has(conversion.id))
continue;` removed from `apply.ts`). Marker 1 to 0, blob `de2efc0c` to
`ee65383a`:
```
Test Files 1 failed | 471 passed | 1 skipped (473)
Tests 2 failed | 13449 passed | 1 skipped (13452)
FAIL a seam can refuse a named entry by id even with `includeRetired: true`
FAIL refuses only the named id — the rest of the chain still runs
AssertionError: the refused entry did not fire: expected undefined to be true
```
Restore: hash back to `de2efc0c`, `git diff HEAD` empty.
## Verification
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **79** commands from the real change
set (5 paths vs merge base `7e74af3df`). All 79 run, all **exit 0**;
`--ran` reconciles 79 derived / 79 run / 0 UNRUN. Three answered **exit
3 (PREREQUISITE NOT MET, read as NOT MEASURED, never a pass)** on the
first pass — `check:dual-build-cjs-loads`, `check:lean-entry-closure`,
`check:type-check-debt` — plus four spec `dist`-readers that refused on
a src/dist digest mismatch; the closure was built (`turbo run build`
over `./packages/*` + `./packages/*/*`, 72 tasks) and all seven re-run
at exit 0.
- `pnpm --filter @objectstack/metadata-core test` — 16 files / **278
tests**, exit 0. `typecheck` exit 0.
- `pnpm --filter @objectstack/spec test` — 473 files / **13453 tests**,
exit 0. `typecheck` exit 0. ⛔ No `--project` narrowing was added by this
branch (spec's own `test` script carries `--project local` itself;
#17853's trap is a narrowing added over a named file, which is not done
here).
- `check:api-surface` is **green without regeneration** — the published
export listing does not move.
- Published-surface measurement, the sound way (grep the BUILT entry
`.d.ts` for symbol NAMES, with a fabricated-name negative control):
`ApplyConversionsOptions` 7 hits, `MetadataConversion` 6,
`applyConversions` 8, fabricated control **0**. ⇒ the symbol set is
UNCHANGED — no export added, moved or removed. What does move is one
line of text inside an existing exported interface:
`excludeConversionIds` appears once in `dist/index.d.ts`.
`check-widening-tells --declaration no` exits 0 (no T1/T2/T3/T4 tell),
and `check-clause2-carriers --pair 17899` exits 0.
- `packages/spec/src/conversions/registry.ts` is NOT in the diff, so the
dispatch's registry-plus-surface fork does not fire by its own terms.
The `.d.ts` text change is reported here anyway, because the seat owns
the declaration and this is the measurement it asked for.
- ⛔ Still NOT measured, and not claimed: no end-to-end boot of a
scaffolded app; the `rest-server.ts:3215` link is read, not executed.
One cheap reading was taken and it only confirms the card's own sentence
— `packages/platform-objects/src/apps/account.app.ts:40` does author
`hidden: true` — while whether a code-declared app enters a compiled
artifact through this door remains unread.
## Acceptance notes
- `noted, not filed:` the same rewrite is reachable at the STORED-ROW
seam for a post-split authored row: `PUT /meta/app/NAME` validates
against `AppSchema`, which accepts `hidden`, so a row written after the
2026-08-09 split with `hidden: true` and no `_unpublished` is
indistinguishable from a pre-split materialized one and is converted on
rehydration. The entry's population argument ("under the old regime a
`hidden: true` row could only have come from the materialization path")
is an argument about the OLD regime only. Not filed: it is the same
ledger question #16864 answered for the flag and would settle the stored
seam's evidence by side effect, which triage ruled out for this card.
承接者: the `domain:spec` seat holding #16864's remaining two carriers
(#17894, #17895).
- `noted, not filed:` the exclusion list is per-door rather than
per-entry, so the NEXT default flip is not covered until somebody adds
its id here. The robust shape is a declaration on the registry entry,
which would move `packages/spec`'s built entry `.d.ts` and re-open the
Clause-② fork this card's dispatch draws. 承接者: whoever takes the general
mechanism follow-up to #16864.
Authored by the `domain:engine` dev round of session
`session_01RuoNSXUbBoWHkNS4AknTrM`
(https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM), dispatched
from seat post #6367.
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>hidden: true app is no longer registered unpublished (#17899)1 parent ee6fbd7 commit 134b410
5 files changed
Lines changed: 296 additions & 3 deletions
File tree
- .changeset
- packages
- metadata-core/src
- spec/src/conversions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
Lines changed: 139 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
263 | 263 | | |
264 | 264 | | |
265 | 265 | | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
266 | 404 | | |
267 | 405 | | |
268 | 406 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
65 | 88 | | |
66 | 89 | | |
67 | 90 | | |
68 | 91 | | |
69 | | - | |
| 92 | + | |
| 93 | + | |
70 | 94 | | |
71 | 95 | | |
72 | 96 | | |
| |||
243 | 267 | | |
244 | 268 | | |
245 | 269 | | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
246 | 293 | | |
247 | 294 | | |
248 | 295 | | |
| |||
289 | 336 | | |
290 | 337 | | |
291 | 338 | | |
| 339 | + | |
292 | 340 | | |
293 | 341 | | |
294 | 342 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
73 | 101 | | |
74 | 102 | | |
75 | 103 | | |
| |||
84 | 112 | | |
85 | 113 | | |
86 | 114 | | |
87 | | - | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
88 | 119 | | |
89 | 120 | | |
90 | 121 | | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
91 | 126 | | |
92 | 127 | | |
93 | 128 | | |
| |||
0 commit comments