Found while widening BaseSchema.hidden under objectui#7455 (maintainer ruling 2026-09-03, option A). Filed unassigned and out of that card's scope: the ruling's scope is BaseSchema on both faces plus the docs row and pins, and this is a different pair.
Part of the same key family as objectui#7530, but ⛔ not a duplicate of it: #7530 asks about the CEL envelope OBJECT form on three keys. This is about the app node taking two different hidden keys, one per face.
The measurement
Measured on @objectstack/spec@17.2.0 (the version the lockfile pins), by resolving AppSchema.shape at runtime:
| key | declared by the spec's AppSchema? | accepts a boolean | accepts a string |
|---|
visible | NO | — | — |
disabled | NO | — | — |
hidden | YES | yes | no |
packages/types/src/zod/app.zod.ts builds the mirror as BaseSchema.extend(SpecAppFields.shape).extend({ ... }), and SpecAppFields excludes six keys — name, label, description, navigation, areas, contextSelectors — with hidden NOT among them. So on the Zod face the spec's hidden lands AFTER the base's and overrides it.
packages/types/src/app.ts declares interface AppComponentSchema extends BaseSchema and does not restate hidden at all, so on the TS face it inherits whatever BaseSchema says.
Why this surfaced now
Before #7455 both faces read boolean and the collision was invisible. #7455 widened BaseSchema.hidden to boolean | string, which moved the TS side of this pair and not the Zod side — so zod-mirror-parity.test.ts turned red naming exactly this pair, and the widening PR seeded a KnownDrift entry recording it.
The same widening applied to visible (objectui#4581) and disabled (objectui#4580 ruling Q3-A) seeded nothing, because the spec's AppSchema declares neither of those keys. hidden is the only one it declares — which is the asymmetry underneath the asymmetry #7455 removed.
The actual question
The two keys collide in NAME and differ in MEANING:
- the spec's
AppSchema.hidden is an app-catalogue flag — whether the app shows in the switcher; BaseSchema.hidden is the renderer's hide predicate, evaluated by SchemaRenderer's shouldHide chain through hasDeclaredPredicate.
Today an author reading AppComponentSchema in TypeScript is told they may write a predicate string; the validator that runs on their metadata refuses it. Whichever way that is resolved, it is a contract decision about two surfaces, not a mechanical repair.
Some directions, ⛔ none of them chosen here:
- Keep the spec's key on both faces for the app node and restate
hidden?: boolean on the TS interface, so the app node stops advertising the renderer's predicate it never had. Cheapest, and it makes the two faces agree; it also means the app node cannot use the predicate spelling every other node can. - Give the app-catalogue flag its own name upstream in
@objectstack/spec and let the renderer's hidden stay inherited. Correct-looking and the most expensive — it is a spec change with its own producers. - Drop
hidden from SpecAppFields so the base's widened key survives on both faces. ⛔ Reads easy and is probably wrong: it makes a spec-derived schema accept, by local divergence, a value the spec refuses.
⛔ Do not close this by editing the ledger
The KnownDrift entry seeded by #7455 records the drift with its reason; it is a ratchet, not a waiver. Deleting the entry without changing either face just hides the collision again.
Refs objectui#7455 · objectui#7530 · objectui#4581 · objectui#4580.
Filed by an ObjectUI development agent (Claude Code) during the implementation of objectui#7455; session reference session_01EMrWaQw3XS5DxTHxp4yRyC.
Found while widening
BaseSchema.hiddenunder objectui#7455 (maintainer ruling 2026-09-03, option A). Filed unassigned and out of that card's scope: the ruling's scope isBaseSchemaon both faces plus the docs row and pins, and this is a different pair.Part of the same key family as objectui#7530, but ⛔ not a duplicate of it: #7530 asks about the CEL envelope OBJECT form on three keys. This is about the app node taking two different
hiddenkeys, one per face.The measurement
Measured on
@objectstack/spec@17.2.0(the version the lockfile pins), by resolvingAppSchema.shapeat runtime:AppSchema?visibledisabledhiddenpackages/types/src/zod/app.zod.tsbuilds the mirror asBaseSchema.extend(SpecAppFields.shape).extend({ ... }), andSpecAppFieldsexcludes six keys —name,label,description,navigation,areas,contextSelectors— withhiddenNOT among them. So on the Zod face the spec'shiddenlands AFTER the base's and overrides it.packages/types/src/app.tsdeclaresinterface AppComponentSchema extends BaseSchemaand does not restatehiddenat all, so on the TS face it inherits whateverBaseSchemasays.Why this surfaced now
Before #7455 both faces read
booleanand the collision was invisible. #7455 widenedBaseSchema.hiddentoboolean | string, which moved the TS side of this pair and not the Zod side — sozod-mirror-parity.test.tsturned red naming exactly this pair, and the widening PR seeded aKnownDriftentry recording it.The same widening applied to
visible(objectui#4581) anddisabled(objectui#4580 ruling Q3-A) seeded nothing, because the spec'sAppSchemadeclares neither of those keys.hiddenis the only one it declares — which is the asymmetry underneath the asymmetry #7455 removed.The actual question
The two keys collide in NAME and differ in MEANING:
AppSchema.hiddenis an app-catalogue flag — whether the app shows in the switcher;BaseSchema.hiddenis the renderer's hide predicate, evaluated bySchemaRenderer'sshouldHidechain throughhasDeclaredPredicate.Today an author reading
AppComponentSchemain TypeScript is told they may write a predicate string; the validator that runs on their metadata refuses it. Whichever way that is resolved, it is a contract decision about two surfaces, not a mechanical repair.Some directions, ⛔ none of them chosen here:
hidden?: booleanon the TS interface, so the app node stops advertising the renderer's predicate it never had. Cheapest, and it makes the two faces agree; it also means the app node cannot use the predicate spelling every other node can.@objectstack/specand let the renderer'shiddenstay inherited. Correct-looking and the most expensive — it is a spec change with its own producers.hiddenfromSpecAppFieldsso the base's widened key survives on both faces. ⛔ Reads easy and is probably wrong: it makes a spec-derived schema accept, by local divergence, a value the spec refuses.⛔ Do not close this by editing the ledger
The
KnownDriftentry seeded by #7455 records the drift with its reason; it is a ratchet, not a waiver. Deleting the entry without changing either face just hides the collision again.Refs objectui#7455 · objectui#7530 · objectui#4581 · objectui#4580.
Filed by an ObjectUI development agent (Claude Code) during the implementation of objectui#7455; session reference
session_01EMrWaQw3XS5DxTHxp4yRyC.