Repository navigation
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
Activity
- addeddomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
on Sep 5, 2026 分诊 —
domain:spec/priority:p3/pm:queue/finding锚定 (anchoring):方向 1(在 TS 接口上重述
hidden?: boolean)与方向 3 都落在packages/types⇒domain:spec。方向 2 是上游 spec 改名,跨仓。在
origin/maina472b07上复核 —— 两个面的构造方式成立packages/types/src/zod/app.zod.ts:271 const SpecAppFields = specFieldsExcept(SpecAppSchema.shape, [ packages/types/src/zod/app.zod.ts:282 * `@objectstack/spec/ui` `AppSchema` (see {@link SpecAppFields}). The drift packages/types/src/zod/app.zod.ts:285 export const AppComponentSchema = BaseSchema.extend(SpecAppFields.shape).extend({ packages/types/src/app.ts:364 export interface AppComponentSchema extends BaseSchema {⭐
zod/app.zod.ts:285的BaseSchema.extend(SpecAppFields.shape)就是机制本身:spec 的键在 base 之后落,所以它覆盖 base 的。而 TS 面app.ts:364只写extends BaseSchema、不重述hidden⇒ 继承 base 的。两个面因此对同一个键取了不同定义。⛔ spec 侧的 runtime 解析(
AppSchema.shape里有hidden、没有visible/disabled)本席验不了(未装 spec),采信未复核。但那正是卡片论证的支点,接手者请重测。⭐ 卡片对"为什么现在才浮现"的解释,是它最有价值的部分
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.以及那条不对称之下的不对称:
The same widening applied to
visible(#4581) anddisabled(#4580 ruling Q3-A) seeded nothing, because the spec'sAppSchemadeclares neither of those keys.hiddenis the only one it declares.⭐ 三个同族键、同一次加宽、只有一个炸出台账项 —— 这个对照本身就是诊断:碰撞不在加宽里,在"spec 恰好也声明了这一个键"里。
定级理由
priority:p3:- 有一个活的 declared ≠ enforced:卡片说得准 —— "an author reading
AppComponentSchemain TypeScript is told they may write a predicate string; the validator that runs on their metadata refuses it." - 但范围极窄:一个键、一个节点类型(app 节点)。对比同族的 finding(types,plugin-kanban):
ObjectKanbanSchemarequiresgroupField(zero read sites) and declares neithergroupBynorlimit— no workingobject-kanbannode is assignable to any declared type #7322(整个object-kanban声明面误导,p2),这里作者会在校验时被响亮拒绝,不是静默不分组。 - 且它已被台账记录(finding(types,docs):
BaseSchema.hiddenis declaredboolean, but the renderer evaluates it as a predicate — the third key onvisible/disabled's evaluated path, never widened #7455 播的KnownDrift项),不是静默漂移。
不上 p2:拒绝是响亮的;影响面是单键单节点。
⛔ 硬禁止(卡片明写,本席背书并加重)
⛔ 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.⚠️ 这条要特别当心,因为删掉台账项是让测试变绿的最快路径,而它恰好是错的。⛔ 三条方向的定型 —— 差别很大,⛔ 不要按"哪条最快"选
方向 落点 型 manual floor 1. TS 面重述 hidden?: booleanpackages/types/src/app.ts收窄 TS 声明使其与验证器一致 ⇒ Bug/tidy 不踩 2. 上游给 app-catalogue flag 改名 @objectstack/spec,跨仓协议变更 踩,且最贵 ⛔ 3. 把 hidden从SpecAppFields排除掉packages/types让本地 schema 接受一个 spec 拒绝的值 踩,且方向错误 ⭐ 卡片对方向 3 的评价我完全同意并复述:"Reads easy and is probably wrong: it makes a spec-derived schema accept, by local divergence, a value the spec refuses." ⛔ 这是最诱人的一条(一行删除、测试变绿),也是唯一会制造新缺陷的一条。
方向 1 是唯一不踩人工底线的,代价卡片也如实说了:app 节点从此用不了其他节点都能用的谓词拼写。⛔ 分诊席不裁决,但把这个代价摆明,因为它是选 1 还是 2 的唯一实质分歧。
⛔ 给执行席的其他边界
- ⛔ 不要与 [Decision] The CEL envelope object is accepted on
visible/hidden/disabledby the shared evaluator but declared on none of them — declare it on all three, or refuse it on all three #7530 合并。卡片已正确划界:那张问的是三个键上的 CEL 信封对象形式,本卡问的是app 节点在两个面上取了两个不同的hidden。相邻、不同。 - 动手前重测 spec 侧(见上),⛔ 别照抄卡片那张表 —— spec 版本会变。
- 若走方向 1,同 PR 删掉
KnownDrift项(此时删是正确的:漂移真的消失了),⛔ 并在 PR 说明里写清楚"app 节点不支持谓词拼写"这个新事实,否则下一个作者会当它是 bug 报回来。
⛔ 分诊席不认领、不派单、不写码、不裁决方向。
Generated by Claude Code
- 有一个活的 declared ≠ enforced:卡片说得准 —— "an author reading
Claim:
domain:spec@ objectui execution seat, sessionsession_01BAZFhALsQsGqxui8sNqM8s, claimed 2026-09-05T17:18Z (PM loop round R1, seventh wave). Assignee set by the PM in the same stroke aspm:queue→pm:dispatched; the dev is a subagent of this session (mode:subagent, no session of its own) — identity is the branch.Branch:
claude/issue-7542-app-hidden-catalogue-flag· worktree../objectui-issue-7542· cut fromorigin/main=16a725f9(17:18Z).
Clause-②: yes — the published TS face narrows on one node:AppComponentSchema.hiddenrestated asboolean(the spec's app-catalogue flag) instead of the inheritedboolean | ExpressionWire; the validator's accept set does not move (it already refuses a predicate on the app node). The PM seat reviews at tier before it lands.
Container & model:fable(CONTRACT_REVIEW_TIER), size S,mode:subagent.Direction: 1, under a declared veto window. No maintainer ruling exists on this card; triage 5548779181 does not rule but classifies the three directions: 1 (restate
hidden?: booleanon the TS interface — Bug/tidy, pulls the declaration back to what the validator enforces, off the manual floor), 2 (rename the app-catalogue flag upstream in@objectstack/spec— a protocol change, manual floor, most expensive), 3 (drophiddenfromSpecAppFields— makes a spec-derived schema accept a value the spec refuses; ⛔ wrong). Direction 1 is the only one inside the seat's remit, is reversible (one restated member + one ledger row + a pin), and does not preclude 2, which stays open to the maintainer as an upstream card. The seat therefore dispatches 1 as its recommended default; a maintainer objection reverts it. Recorded in the decision batch.Scope:
packages/types/src/app.ts— restatehidden?: booleanonAppComponentSchemawith a docblock saying it is the specAppSchema's app-catalogue flag (whether the app shows in the switcher), NOT the renderer's hide predicate, that the predicate spelling every other node accepts is refused here on both faces, and citing #7542;packages/types/src/__tests__/zod-mirror-parity.test.ts— delete theKnownDriftentry'app.zod.ts#AppComponentSchema': 'hidden'(:932) in the same PR (correct now: the drift disappears — ⛔ never without the face moving); new pin; docs row if a page listshiddenfor the app node (measure); changeset@object-ui/types: minor. ⛔app.zod.tsuntouched (direction 3 forbidden), ⛔BaseSchema.hiddenuntouched (#7455 / #7530), ⛔ not folded with #7530.Premise verified on
origin/main16a725f9at 17:18Z:zod/app.zod.ts:279SpecAppFields = specFieldsExcept(SpecAppSchema.shape, ['name','label','description','navigation','areas','contextSelectors'])—hiddennot excluded;:293AppComponentSchema = BaseSchema.extend(SpecAppFields.shape).extend({…})— the spec'shiddenlands after the base's;app.tsexport interface AppComponentSchema extends BaseSchemarestates nohidden;base.ts:376hidden?: boolean | ExpressionWireandbase.zod.ts:200z.union([z.boolean(), ExpressionWireSchema])(the #7530 CEL envelope — the TS face is wider than the card'sboolean | stringreading now);KnownDriftrow atzod-mirror-parity.test.ts:932, seeded by #7455. The spec-side reading (AppSchema.shape.hiddenaccepts boolean, refuses string; novisible/disabled) is the card's and is NOT re-verified by the seat (node resolution of the pin from the shared checkout failed) — the dev re-measures it through the installed pin first.File surface (region level under the 10:5xZ ruling, comment 5551274423):
app.ts— theAppComponentSchemablock;zod-mirror-parity.test.ts— the oneKnownDriftrow; new pin; one docs row if any; changeset. Hot files:zod-mirror-parity.test.tsis shared with PR #7776 (#7279, header /UnmirroredDeclareddocblock /SPEC_DERIVED_PAIRScomment / a new describe — different regions; it lands first) — mergemainbefore opening and after it lands. Half-state patrol anchor: none exists in objectui (#5986).
Generated by Claude Code
Claim (dev): branch
claude/issue-7542-app-hidden-catalogue-flag· worktree../objectui-issue-7542· cut fromorigin/main=669d71bf(17:25Z),origin/main1b69ca58(PR #7776) merged before opening · dev of sessionsession_01BAZFhALsQsGqxui8sNqM8s's dispatch (mode:subagent, no session of its own — identity is the branch). Assignee is the PM's and is not touched.Premise re-verified on the tree, all five points of the claim hold; the spec-side reading that the seat could not take is now measured through the installed pin (
@objectstack/spec@17.2.0,uientry):AppSchema.shape.hiddenisoptional(boolean), acceptstrue/false, refuses a string and the CEL envelope object withinvalid_typeathidden;visibleanddisabledare absent from the shape (30 keys; controlslabelandactivepresent). Both entry paths (AppComponentSchema.safeParse,safeValidateSchema) refused the predicate string and the envelope athiddenbefore editing and accepted the booleans — recorded in the PR body. Direction 1 implemented;app.zod.tsandBaseSchema.hiddenuntouched; theKnownDriftrow goes only because the face moved (both directions ablated).
Generated by Claude Code
os-dev-report
{ "issue": 7542, "status": "done", "branch": "claude/issue-7542-app-hidden-catalogue-flag", "pr": "https://github.com/objectstack-ai/objectui/pull/7782", "head": "1d3074f8 (= 6bd44196 implementation + merge of origin/main 1b69ca58 / PR #7776; base 669d71bf, whose packages/types/src/app.ts blob 14d12b7c equals 16a725f9's)", "premise_still_valid": true, "clause_2": "yes — the published TS face narrows on one node: AppComponentSchema.hidden restated boolean (was the inherited boolean | ExpressionWire); the validator's accept set does not move", "line_budget": "n/a", "tier": "CONTRACT_REVIEW_TIER; label needs:contract-review attached by MCP label union (REST additive endpoint 403 in this session), read back as [package: types, tests, needs:contract-review] = the union, nothing stripped; PR left draft; not governed (0 of 4 paths, objectstack d701e65)", "summary": "Direction 1 implemented under the seat's declared veto window: packages/types/src/app.ts restates hidden?: boolean on the AppComponentSchema block with a docblock naming the collision (spec AppSchema app-catalogue flag taken by reference through SpecAppFields vs BaseSchema's renderer predicate), the refusal of the predicate spelling on both faces, the in-repo reader (filterActiveApps reads hidden !== true, never evaluates), direction 1 as the ruling and direction 2 as the open alternative. The KnownDrift row 'app.zod.ts#AppComponentSchema': 'hidden' is deleted in the same PR because the drift is gone (both faces boolean); header counts 42/63 to 41/62 with a history sentence added in the cumulative style and the dangling 'see that entry' pointer repointed. New pin app-hidden-catalogue-flag-7542.test.ts (8 tests). Changeset @object-ui/types minor. app.zod.ts and BaseSchema.hidden untouched; docs measured, no app-node hidden row exists (the three app-schema.mdx hits are AppMenuItem.hidden), so none moved. Assignee not touched; the PM's assignee field was empty on the payload read, as expected for the shared identity.", "spec_reading": "@objectstack/spec@17.2.0 ui entry resolved from packages/types/node_modules (dist/ui/index.js): AppSchema.shape has 30 keys; hidden present, optional(boolean), described 'Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead (navigation only — never an access gate)'; visible ABSENT, disabled ABSENT (strict schema refuses them as unrecognized_keys); controls label and active present. safeParse hidden true/false OK; predicate string REFUSED invalid_type at hidden 'expected boolean, received string'; CEL envelope object REFUSED invalid_type at hidden 'expected boolean, received object'.", "entry_path_verdicts": { "before_669d71bf": "AppComponentSchema.safeParse: hidden true/false OK; predicate string REFUSED invalid_type at path hidden; envelope REFUSED invalid_type at path hidden (received object). safeValidateSchema: true/false OK; string REFUSED root invalid_union with the app arm carrying the hidden issue; envelope REFUSED the same way. Controls: BaseSchema and ButtonSchema accept string and envelope on hidden; safeValidateSchema accepts a button with the string.", "after_1d3074f8": "runtime identical (validator unchanged); declaration moved: AppComponentSchema['hidden'] is boolean | undefined (invariant Equal pinned), a predicate string on a typed app document is a compile error (ts-expect-error pinned)." }, "files_changed": [ "packages/types/src/app.ts (+44: hidden?: boolean with docblock, AppComponentSchema block only)", "packages/types/src/__tests__/zod-mirror-parity.test.ts (-28 row+docblock, header count line 42/63 to 41/62 + history sentence, one pointer repointed)", "packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts (new, 265 lines, 8 tests)", ".changeset/app-hidden-catalogue-flag-7542.md (@object-ui/types: minor)" ], "gates": [ {"command": "pnpm --filter '@object-ui/types^...' build", "exit": 1, "head": "669d71bf", "verdict": "ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT — closure is @object-ui/test-support with no build script; nothing to build (n/a, not red)"}, {"command": "pnpm --filter @object-ui/types build", "exit": 0, "head": "1d3074f8", "verdict": "dist completeness: 1 package(s) complete (124 emitted files verified)"}, {"command": "pnpm --filter @object-ui/types type-check (tsc, tsconfig.examples.json, tsconfig.test.json)", "exit": 0, "head": "1d3074f8", "verdict": "clean"}, {"command": "pnpm exec vitest run --maxWorkers=2 packages/types/", "exit": 0, "head": "1d3074f8", "verdict": "Test Files 126 passed (126) · Tests 2233 passed (2233)"}, {"command": "pnpm --filter @object-ui/types lint", "exit": 0, "head": "1d3074f8", "verdict": "270 problems (0 errors, 270 warnings) — pre-existing no-explicit-any, none in the four touched files"}, {"command": "node scripts/check-changeset-presence.mjs", "exit": 0, "head": "1d3074f8", "verdict": "3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/app-hidden-catalogue-flag-7542.md"}, {"command": "node scripts/check-changeset-fixed.mjs · check-changeset-no-major.mjs · check-changeset-overwrite.mjs", "exit": "0 / 0 / 0", "head": "1d3074f8", "verdict": "green (No changeset declares a major bump; No pre-existing changeset was modified or deleted)"}, {"command": "node scripts/check-control-bytes.mjs", "exit": 0, "head": "1d3074f8", "verdict": "check-control-bytes: OK (scanned 6342 tracked text file(s); skipped 85 binary)"}, {"command": "pnpm check:spec-symbols · check:doc-types · check:doc-fences", "exit": "0 / 0 / 0", "head": "1d3074f8", "verdict": "green"}, {"command": "node /home/user/objectstack/scripts/pm/check-governed-merges.mjs --test (final four paths)", "exit": 0, "head": "objectstack d701e65", "verdict": "0 of 4 path(s) hit the register — NOT governed"}, {"command": "pnpm --filter @object-ui/app-shell type-check (read-only consumer sweep)", "exit": 2, "head": "6bd44196", "verdict": "NOT MEASURED — 934 TS2307 'Cannot find module @object-ui/components…' errors: app-shell resolves 29 sibling packages via dist declarations absent in a fresh worktree; zero errors mention hidden or AppComponentSchema; declared narrowing, CI measures it"}, {"command": "node scripts/check-doc-links.mjs", "exit": "not run", "head": "-", "verdict": "no docs row moved"}, {"command": "CI on PR #7782", "exit": "in_progress", "head": "1d3074f8", "verdict": "not awaited (report at PR-open time)"} ], "tests": "Targeted (pre-merge, 6bd44196): vitest app-hidden-catalogue-flag-7542 + zod-mirror-parity → 2 files, 26 tests passed. Final union at 1d3074f8 under the shared verify lock (shared-box seconds, 33s held): build 0, type-check 0, vitest packages/types/ 0 (126 files, 2233 tests). Ablation at 6bd44196 under the lock, trap-restored, absolute paths, blob-hash proof — Leg A: app.ts checked out from 669d71bf (git hash-object 14d12b7c = base blob, 'hidden?: boolean;' count 0, docblock marker count 0) → tsc -p tsconfig.test.json exit 2 with exactly 3 errors: app-hidden-catalogue-flag-7542.test.ts(99,3) TS2344 'false' does not satisfy 'true' (the Equal leg), (186,5) TS2578 unused ts-expect-error, zod-mirror-parity.test.ts(1849,14) TS2322 '\"app.zod.ts#AppComponentSchema\"' is not assignable to 'never' (the parity file naming the pair); vitest on the two files stayed green 26/26 (mutation is on the TS face; runtime legs measure the mirror — expected direction, reported as observed); restored via git checkout HEAD -- PATH: hash b4010c1e = HEAD blob, count 1, git diff HEAD empty. Leg B: the KnownDrift row put back from 669d71bf with the face narrowed (hash 6a35c73e = base blob, row count 1) → tsc exit 2 with 1 error: zod-mirror-parity.test.ts(1871,14) TS2322 naming the pair; vitest exit 1, 1 failed | 25 passed (the pin's ledger leg); restored: hash 9147eee6 = HEAD blob, row count 0, git diff HEAD empty, git status clean. Resolution path is source (../app, ../zod/app.zod, tsconfig.test.json) — no dist in the loop, dist preflight n/a.", "mcp_calls": "9 — create_pull_request, add_issue_comment (claim), issue_read get_labels on the PR number (failed: not resolvable as an Issue), pull_request_read get (body read-back), issue_read get_comments (claim read-back), issue_write update (label union), pull_request_read get (label read-back), add_issue_comment (this report), issue_read get_comments (report read-back). Issue body + triage + PM claim read through the zero-quota public-page payload (frontTimelineItems hasNextPage false, 15 items); REST read probe returned 403 (GitHub access not enabled for this session).", "deviations": [ "REST channel 403 for this session: reads went git → public payload; label write went MCP union with compare read-back instead of the additive REST endpoint; declared here as the channel switch.", "app-shell read-only type-check NOT MEASURED locally (dist closure of 29 packages absent in the fresh worktree; build does not fit the foreground cap); substitute evidence: grep sweep — no source in app-shell/layout/console authors hidden on an app-typed document, the only AppComponentSchema consumer (useNavigationSync.ts) writes no hidden, and filterActiveApps reads hidden !== true.", "Ablation revert target was 669d71bf, not 16a725f9 as the dispatch spelled it: app.ts blob is identical at both (14d12b7c), so the leg is equivalent; the parity file was reverted to 669d71bf for leg B.", "PR body ends with the system-reminder attribution form (🤖 Generated with … + session URL line); the platform appended a second session-URL footer on create (the known create-time behaviour) — recorded, not repaired.", "The vitest half of leg A stayed green — the dispatch's 'the parity suite goes red naming the pair' was observed under tsc -p tsconfig.test.json (type-level LedgerMismatch), not under vitest; both instruments reported." ], "open_questions": [ {"question": "Direction 2 (rename the app-catalogue flag upstream in @objectstack/spec so the app node can inherit the renderer's predicate) remains open to the maintainer; nothing in this PR precludes it.", "options": ["A — leave direction 1 landed and file/keep an upstream spec card for the rename", "B — veto direction 1 and take direction 2 first (cross-repo, with producers)"], "recommendation": "A, because the in-repo readers and the spec producer already treat the key as a boolean, direction 1 is one member + one row + one pin and reversible by one revert, and direction 2 is a protocol change with its own producers"} ], "out_of_scope_findings": [] }
Generated by Claude Code
Contract review — PASS at
CONTRACT_REVIEW_TIER(in-seat, 2026-09-05T17:50Z) — PR #7782, head1d3074f8Implemented-by: dev subagent of this session's dispatch, branch
claude/issue-7542-app-hidden-catalogue-flag(mode:subagent, no session of its own); dev report 5553657368
Reviewed-by:session_01BAZFhALsQsGqxui8sNqM8s(domain:spec@ objectui seat; served tier read viaget_sessionat 17:49Z:session_context.modelandlast_served_modelbothclaude-fable-5-1)Diff read by ref against
origin/main1b69ca58(four files, +324 / −30; theapp.tshunk, the parity-file hunk, the changeset and the pin's describe/it list re-read off the branch tip withgit diff origin/main...at 17:49Z;app.zod.tsuntouched — verified, 0 lines).① Derived judgments — each accept-set and public-surface change named and judged
packages/types/src/app.ts,AppComponentSchemablock only:hidden?: boolean;restated with a docblock naming the collision (specAppSchema.hiddenapp-catalogue flag taken by reference throughSpecAppFieldsvsBaseSchema's renderer predicate), the spec reading through the installed pin (AppSchema.shape.hidden=z.boolean().optional(), the spec's description quoted;visible/disabledabsent — which explains why finding(types): BaseSchema under-declares visible (predicate string) and ariaLabel (I18nLabel) — the renderer supports both, five test fixtures now cast past the gap #4581 / finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580's widenings reached this node and finding(types,docs):BaseSchema.hiddenis declaredboolean, but the renderer evaluates it as a predicate — the third key onvisible/disabled's evaluated path, never widened #7455's did not), both refusals measured before the edit (safeParseandsafeValidateSchema), the in-repo reader (filterActiveAppskeeps an app out of the launcher onhidden !== true, never evaluates), direction 1 as the ruling and direction 2 as the open alternative. Every other member kept. Accept set: the TS face narrows on one node (a predicate string / CEL envelope on a typedappdocument is now a compile error); the validator's accept set does not move — it refused those spellings at pathhiddenbefore and after, on both entry paths, measured withBaseSchema/ButtonSchemacontrols that still accept them. This is direction 1 exactly as claimed: the declaration pulled back to what the validator has enforced all along; ⛔ direction 3 not taken (app.zod.tsuntouched, verified),BaseSchema.hiddenuntouched on both faces.zod-mirror-parity.test.ts: theKnownDriftrow'app.zod.ts#AppComponentSchema': 'hidden'and its docblock deleted in the same PR — correct, the drift is gone (both facesboolean), and leg B of the ablation proves the ledger refuses the deletion alone (row restored with the face narrowed → TS2322 naming the pair + the pin's ledger leg red). Header counts42 / 63→41 / 62with a history sentence ADDED in the cumulative style; the dangling "see that entry" pointer re-pointed. No conflict with PR test(types): re-derive ObjectViewSchema's side of the #6058 split as LOCAL and pin the header counts (objectui#7279) #7776's regions (merged before opening, clean). Triage's hard prohibition honoured.- New pin
app-hidden-catalogue-flag-7542.test.ts(8 tests): invariantEqualofAppComponentSchema['hidden']withboolean | undefined;BaseSchema['hidden']stillboolean | ExpressionWire | undefinedandButtonSchema['hidden']equal to it (controls); 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; controls onBaseSchema,ButtonSchemaand the union path with abutton; the spec reading live through the pin with control keyactive; theKnownDriftrow absent off disk beside a control that the reader sees the ledger and the pair stays inMIRRORS. Ablation: leg A (app.tsreverted to the base blob by hash) →tsc -p tsconfig.test.jsonexit 2 with exactly three errors (theEqualleg, the unused@ts-expect-error, the parity file's TS2322 naming the pair) while the runtime legs stay green — the right asymmetry for a TS-face-only change, reported as observed; leg B as above; restores proven by blob hash,git diff HEADempty. Adequate and red-first on both instruments. - Docs: measured, no row exists for
hiddenon the app node (the threeapp-schema.mdxhits areAppMenuItem.hidden) — none moved;check-doc-linksnot run for that reason. Accepted. - Parity suite green on the merged head (126 files / 2233 tests); three tsc projects green; lint 0 errors on the four touched files.
② semver —
.changeset/app-hidden-catalogue-flag-7542.md:@object-ui/types: minor, naming the collision, the ruling direction, the TS-only breaking edge (a predicate on anappnode no longer type-checks; the app node cannot use the predicate spelling), that authored JSON metadata is unaffected (the validator never admitted it), the reader's agreement, and the removedKnownDriftrow. A TS-face narrowing is theminorclass this seat has graded all shift; consistent. The card's own requirement (triage boundary 3: write down the new fact that the app node does not support the predicate spelling) is met in the changeset, the docblock and the PR body.③ Boundary flags —
open_questions: one — direction 2 (upstream rename) stays open; the dev recommends A (leave direction 1 landed, keep an upstream card if the maintainer wants it). That is the veto-window arrangement the claim declared (5553481421), recorded in the decision batch as item 8; no new maintainer item. Deviations reviewed and accepted (REST 403 channel fallbacks with read-backs; the app-shell read-only type-check NOT MEASURED — a fresh worktree lacks the 29 sibling dists, 934 unrelated TS2307s, zero mentioninghidden/AppComponentSchema, with the grep sweep standing in and CI's farm measuring it; ablation revert target669d71bfwhoseapp.tsblob equals16a725f9's; thepnpm --filter '@object-ui/types^...' buildnon-red — the closure has no build script). No out-of-scope findings. Nothing touchescontent/docs/releases/.Verdict: PASS for PR #7782. Same stroke:
needs:contract-reviewis cleared on the PR with a provenance comment citing this PASS. Landing follows once every check run on1d3074f8iscompleted: success(farm started 17:45Z; Type Check, four shards, Lint, Doc Snippet Type Check, Bundle Analysis, Skill Example Check, README Export Check in progress at 17:48Z) — flip re-check armed. Head moves ⇒ the label is re-hung and the delta re-reviewed.
Generated by Claude Code
Landing — PR #7782 (2026-09-05T18:01Z). Pre-landing three: ① flipped ready at 18:01Z after the PASS above (comment 5553667716) and the carrier cleared on the PR with provenance (PR comment 5553670229); every check run on
1d3074f8wascompleted: successorskippedat 18:00Z (32 runs), branch tip re-read1d3074f8at 18:00:52Z; ② auto-merge enabled, method SQUASH (the API echoes an empty method string — known platform reading); ③ queue probe running againstrefs/heads/gh-readonly-queue/main/*(the queue was empty at 18:00Z after PR #7777 landed;origin/main=554f2b6b). This PR carriesFixes #7542: the card closes on its squash;pm:dispatchedis stripped once MERGED is read twice (PRmerged: true+origin/maincontaining the squash). Direction 1 lands under the veto window declared on the claim; direction 2 (upstream rename) stays open to the maintainer — recorded in the decision batch as item 8.
Generated by Claude Code
MERGED (2026-09-05T18:17Z) — PR #7782 landed through the merge queue as squash
2fcefb95onmain. Three readings: PRmerged: true(merged_at18:16:31Z); this card'sclosed_by_pull_requestsreference to PR #7782 readsMERGEDand the card closed at 18:16:32Z;git log origin/mainat 18:17:10Z shows2fcefb95 feat(types): restate AppComponentSchema.hidden as the spec's app-catalogue boolean (objectui#7542) (#7782)at the tip.pm:dispatchedstripped in this stroke (remaining:finding,priority:p3,domain:spec). Direction 1 is onmainunder the veto window declared on the claim (5553481421): a maintainer objection reverts it by one revert (the restated member, the removedKnownDriftrow, the pin, the changeset); direction 2 (renaming the app-catalogue flag upstream in@objectstack/spec) stays open and is recorded in the seat's decision batch as item 8. Sessionsession_01BAZFhALsQsGqxui8sNqM8s.
Generated by Claude Code
Maintainer ruling recorded — direction 2 = A: direction 1 stays landed; ⛔ no upstream rename card. The veto window declared on the claim is now closed.
Recorded by the
domain:spec@ objectui seat, sessionsession_01BAZFhALsQsGqxui8sNqM8s, 2026-09-06T02:30Z. Provenance (who / verbatim / where): the maintainer (os-justin), live PM chat with this session, replying to decision batch 2 of 2026-09-05T23:1xZ, in which this card was item 8 — tabled with the seat's recommendation A and the four-facet analysis in Chinese. Verbatim reply: 「第二批(4 项,契约形状族) 帮我处理,全部同意」.What this settles. PR #7782 (
2fcefb95, merged 2026-09-05T18:16Z) landed direction 1 under a declared veto window:AppComponentSchema.hiddenis restated on the TypeScript face as the specAppSchema's app-catalogue boolean, matching what the validator has always enforced; theKnownDriftrow is gone because the drift is gone. The window closes with this ruling — direction 1 is now a decision, not a default awaiting objection.Direction 2 is declined, on the record: no card is filed upstream in
@objectstack/specto rename the app-catalogue flag so the app node could inherit the renderer'shiddenpredicate. The four facets as tabled: no measured pull (the only in-repo reader,filterActiveApps, testshidden !== true; the spec producer writes a boolean; nothing asks to hide an app by expression), one name one meaning is already achieved, the TypeScript face and the validator now agree so an authored predicate is a compile error at the site, and a protocol change with its own producers for a hypothetical need is capability expansion the startup-focus rule refuses.Consequence for authors, stated so it is not re-reported as a bug: on an
appnode,hiddenis the app-switcher catalogue flag and takestrue/falseonly; the predicate spelling every other node accepts (hidden: "user.role == 'admin'", or the CEL envelope) is refused on both faces there. That is recorded in the member's docblock, in PR #7782's changeset and now here. The card stays closed; no label change (finding,priority:p3,domain:specretained).
Generated by Claude Code
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.