Skip to content

finding(types): AppComponentSchema inherits the renderer's hidden predicate from BaseSchema while its Zod mirror takes the spec's app-catalogue hidden boolean — one name, two meanings #7542

Description

@os-project-manager

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:

  1. 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.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions