…logue boolean (objectui#7542)
On the app node two keys share the name `hidden`: BaseSchema's renderer
hide predicate (`boolean | ExpressionWire`) and the spec AppSchema's
app-catalogue flag (`z.boolean().optional()`, "Hide from the App
Switcher"). The zod mirror takes the spec's key by reference through
SpecAppFields, which lands after the base's and overrides it, so the
validator has always refused a predicate on an app document; the TS
interface restated nothing and inherited the base's union, inviting the
spelling the validator refused. Direction 1 of the card: restate
`hidden?: boolean` on the TS face so the declaration says what the
validator enforces.
- packages/types/src/app.ts: `hidden?: boolean` with the docblock naming
the collision, both refusals, the in-repo reader (`filterActiveApps`,
`hidden !== true`) and direction 2 as the open alternative.
- zod-mirror-parity.test.ts: the KnownDrift row objectui#7455 seeded for
`app.zod.ts#AppComponentSchema` is removed because the drift is gone
(both faces read boolean); header counts 42/63 -> 41/62 with the
history sentence added, not rewritten.
- new pin app-hidden-catalogue-flag-7542.test.ts: invariant Equal on the
app node plus base/button controls, a ts-expect-error on a predicate
string, both entry paths refusing string and CEL envelope at path
`hidden`, the spec reading live through the installed pin with a
control key, and the ledger row absent read off disk.
- changeset: @object-ui/types minor.
Measured on @objectstack/spec@17.2.0 through the installed pin:
AppSchema.shape.hidden is optional(boolean); visible/disabled absent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Fixes #7542
What moves
AppComponentSchema.hiddenon the TypeScript face is restated asboolean— the spec's app-catalogue flag — instead of theboolean | ExpressionWirehide predicate every other node inherits fromBaseSchema. Direction 1 of the card, dispatched by thedomain:specseat under a declared veto window (the claim comment on the card); no maintainer ruling exists on the card, and direction 2 (renaming the catalogue flag upstream in@objectstack/spec) stays open as the alternative.Tier
CONTRACT_REVIEW_TIER, Clause-② yes: a published TS face narrows on one node. The validator's accept set does not move.The collision, measured
On the
appnode two keys share one name and mean different things:BaseSchema.hidden— the renderer's hide predicate (boolean | ExpressionWire; objectui#7455 widened it to the string, objectui#7530 to the CEL envelope), evaluated bySchemaRenderer'sshouldHidechain.@objectstack/spec/uiAppSchema.hidden— the app-catalogue flag, described by the spec as "Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead (navigation only — never an access gate)".packages/types/src/zod/app.zod.tsbuilds the mirror asBaseSchema.extend(SpecAppFields.shape).extend(...), andSpecAppFields = specFieldsExcept(SpecAppSchema.shape, ['name', 'label', 'description', 'navigation', 'areas', 'contextSelectors'])does not excludehidden, so the spec's boolean lands after the base's key and overrides it.packages/types/src/app.tsrestated nothing and inherited the base's union: the published declaration invited a spelling the published validator refused.Spec side, through the installed pin (
@objectstack/spec@17.2.0,uientry, resolved frompackages/types/node_modules):AppSchema.shapehas 30 keys;hiddenis present asoptional(boolean);visibleanddisabledare ABSENT (the strict schema refuses them asunrecognized_keys); control keyslabelandactivepresent.AppSchema.safeParsewithhidden: true/false→ OK; with the predicate string →invalid_typeathidden, "expected boolean, received string"; with the CEL envelope object →invalid_typeathidden, "expected boolean, received object".Both entry paths, BEFORE editing (source via
tsx,origin/main669d71b), document{ type: 'app', name: 'crm', label: 'CRM', hidden: VALUE }:AppComponentSchema.safeParsesafeValidateSchematrue/falseinvalid_typeat pathhidden, expected booleaninvalid_union; theapparm carries the samehiddenissueinvalid_typeat pathhidden, expected boolean, received objectinvalid_union, same arm issueControls, same run:
BaseSchemaandButtonSchemaaccept the string and the envelope onhidden;safeValidateSchemaaccepts abuttonwith the string. The refusal is the app arm's, not the union's.AFTER (runtime): identical — the validator did not move. What moved is the declaration:
AppComponentSchema['hidden']now readsboolean | undefined, and a predicate string on a typedappdocument is a compile error.The in-repo reader agrees with the boolean.
filterActiveApps(packages/app-shell/src/utils/appRoute.ts) keeps an app out of the launcher ona?.hidden !== trueand never evaluates the value;UnifiedSidebar.tsxfilters the switcher the same way. A predicate string on this node would have read as "not hidden" without a sound. No source inpackages/app-shell,packages/layoutorapps/consoleauthorshiddenon an app-typed document (the onlyAppComponentSchemaconsumer,useNavigationSync.ts, spreadsnavigationand writes nohidden).New fact for the next author (triage's requirement): an
appnode cannot use the predicate spelling every other node accepts. That is not a bug in this PR; it is the spec's key winning on this node on both faces. The renderer'shiddenpredicate on every other node is unchanged, andBaseSchema.hiddenis untouched.Files
packages/types/src/app.ts— theAppComponentSchemablock only:hidden?: boolean;with the docblock naming the collision, the spec reading, both refusals, the in-repo reader, direction 1 and direction 2, and the pin. Every other member kept.packages/types/src/__tests__/zod-mirror-parity.test.ts— the oneKnownDriftrow'app.zod.ts#AppComponentSchema': 'hidden'(with its docblock) deleted, because the drift is gone: both faces readboolean. Header counts42 / 63→41 / 62with a history sentence ADDED in the cumulative style (the ledger's first shrink on a SPEC-DERIVED pair, and the first by moving the declared face toward the mirror); the pointer "see that entry" in the objectui#7455 history line now reads "the entry objectui#7542 removed, above". Nothing else in the file. ⛔ The row went only because the face moved — leg B of the ablation below proves the ledger would have refused the deletion alone.packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts— new pin (8 tests): invariantEqualofAppComponentSchema['hidden']withboolean | undefined, controls thatBaseSchema['hidden']is stillboolean | ExpressionWire | undefinedandButtonSchema['hidden']equals it; a@ts-expect-erroron a predicate string authored on a typed app document; the mirror acceptingtrue/falseand refusing the string and the envelope AT PATHhiddenon both entry paths (union arm errors flattened); controls onBaseSchema,ButtonSchemaand the union path with abutton; the spec reading live through the pin with control keyactive; theKnownDriftrow absent, read off disk, beside a control that the reader sees the ledger (CalendarViewSchemarow) and that the pair is still registered inMIRRORS..changeset/app-hidden-catalogue-flag-7542.md—@object-ui/types: minor, naming the collision, the ruling direction, the TS-only breaking edge, and that authored JSON metadata and every other node are unaffected.app.zod.tsuntouched (direction 3 forbidden);BaseSchemauntouched on both faces; docs — measured, no row exists: the threehiddenhits incontent/docs/core/app-schema.mdxare allAppMenuItem.hidden(navigation items), andapp-shell.mdx/schema-reference.mdlist nohiddenon the app node, so no docs row moved.Gates (exit codes captured before any pipe; heavy runs under the shared verify lock; final union at
1d3074f8, the merged head)pnpm --filter '@object-ui/types^...' build(dependency closure)ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT— the closure is@object-ui/test-support, which has nobuildscript; nothing to build, not a redpnpm --filter @object-ui/types builddist completeness: 1 package(s) complete (124 emitted files verified)pnpm --filter @object-ui/types type-check(three tsc projects incl.tsconfig.test.json)pnpm exec vitest run --maxWorkers=2 packages/types/Test Files 126 passed (126) · Tests 2233 passed (2233)pnpm --filter @object-ui/types lintno-explicit-anywarnings name none of the four touched filesnode scripts/check-changeset-presence.mjs3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-fixed/-no-major/-overwritenode scripts/check-control-bytes.mjsOK (scanned 6342 tracked text file(s))pnpm check:spec-symbols/check:doc-types/check:doc-fencescheck-governed-merges.mjs --test, objectstackd701e65, on the final four paths)0 of 4 path(s) hit the register — NOT governedpnpm --filter @object-ui/app-shell type-check(read-only, the app node's consumer)TS2307 Cannot find module '@object-ui/components'… errors: app-shell resolves its 29 sibling packages through theirdistdeclarations, which a fresh worktree does not have, and the closure build does not fit the foreground cap. Zero of the 934 errors mentionhiddenorAppComponentSchema. Declared narrowing: the grep sweep above stands in (no consumer authorshiddenon an app-typed document), and CI's type-check farm measures itnode scripts/check-doc-links.mjsnot run: no docs row moved.Ablation — red-first, both directions, blob-hash proof (under the lock, at
6bd44196, trap-restored, absolute paths)app.tsreverted to the base blob (git checkout 669d71bf -- packages/types/src/app.ts; on diskgit hash-object=14d12b7c…= the base blob,hidden?: boolean;count 0):tsc -p tsconfig.test.jsonexit 2 with exactly three errors —app-hidden-catalogue-flag-7542.test.ts(99,3) TS2344 Type 'false' does not satisfy the constraint 'true'(theEqualleg),(186,5) TS2578 Unused '@ts-expect-error' directive, andzod-mirror-parity.test.ts(1849,14) TS2322 Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'— the parity file naming the pair. vitest on the two files stayed green (26/26): the mutation is on the TS face and the runtime legs measure the mirror, which is the expected direction, reported as observed. Restored withgit checkout HEAD -- PATH: hash =b4010c1e…= the HEAD blob, count 1,git diff HEADempty.KnownDriftrow put back with the face narrowed (git checkout 669d71bf -- zod-mirror-parity.test.ts; hash =6a35c73e…= the base blob, row count 1):tsc -p tsconfig.test.jsonexit 2 with one error —zod-mirror-parity.test.ts(1871,14) TS2322 Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'(recorded drift, none measured); vitest exit 1,1 failed | 25 passed— the new pin's ledger leg. Restored: hash =9147eee6…= the HEAD blob, row count 0,git diff HEADempty,git statusclean.../app,../zod/app.zod,tsconfig.test.json), nodistin the loop, so the dist preflight does not apply; the on-disk proof is the blob hash and the grep counts above.Overlap
origin/mainmerged before opening (1d3074f8= this branch ∪1b69ca58, PR #7776 for objectui#7279, which edits other regions ofzod-mirror-parity.test.ts— merge was clean, no conflict; its new describe pins theUnmirroredDeclaredsplit figures and reads nothing fromKnownDrift). The final union above ran on the merged head. objectui#7530 is not folded in (different question: the envelope object on three keys); objectui#7455 and objectui#4581 / objectui#4580 are referenced only.Why direction 1, on the four axes (for the veto window)
hidden(filterActiveApps,UnifiedSidebar) treat it as the catalogue boolean; nobody authors a predicate on an app node; the spec's producer declares a boolean. Direction 1 declares what is read and validated.Placeholders in this body are spelled as words (
PATH,VALUE) because the GitHub sanitizer strips angle-bracket shapes on save. Session:https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s.🤖 Generated with Claude Code
https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Generated by Claude Code