feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(types): declare the predicate string BaseSchema.hidden already evaluates - #7543

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen
Sep 3, 2026
Merged

feat(types): declare the predicate string BaseSchema.hidden already evaluates#7543
os-project-manager merged 3 commits into
mainfrom
claude/issue-7455-hidden-predicate-widen

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#7455

Implements the maintainer ruling recorded 2026-09-03 on that card (option A: widen), decision batch #24.

Clause-②: yes — a published type surface widens. Draft + needs:contract-review; ⛔ not marked ready, no auto-merge.

Measurements below are at f7b8948b1, the head of this branch, taken after origin/main was merged in.

What changed

filebeforeafter
packages/types/src/base.tshidden?: booleanhidden?: boolean | string
packages/types/src/zod/base.zod.tsz.boolean()z.union([z.boolean(), z.string()])
content/docs/api/schema-reference.md:72"Boolean only — unlike visible, this key takes no expression."states what the renderer does; hiddenOn named as the sibling spelling
SchemaRenderer.hiddenDeclaredGate.test.tsxthe string form went through a Record helper and a castthe string form goes through a typed, cast-free helper

Plus the JSDoc block on hidden (recording the reasoning the way visible and disabled's blocks already do, and noting that ADR-0089 governs packages/spec's keys and not this surface), a new types-side pin, one KnownDrift entry explained below, and a minor changeset on @object-ui/types.

⛔ This is not "consistent with objectui#4581 / objectui#4580-Q3-A", and the PR does not say that

The precedent is incomplete, and the triage on the card measured why. The CEL envelope object form is accepted by the shared evaluator on all three keys and is declared on none of them — visible and disabled are boolean \| string and under-report it exactly as hidden did. objectui#7530 carries that question for all three together.

So, per key, after this PR:

  • visible — boolean and string declared on both faces; envelope undeclared.
  • hidden — boolean and string declared on both faces as of this PR; envelope undeclared.
  • disabled — boolean and string declared on both faces; envelope undeclared.

⛔ Nothing here declares the envelope on hidden. The pin that exercises it keeps the helper and the cast it already used, deliberately.

Verification I re-derived rather than inherited

The three keys' shapes on this branch's base (d04e79a80), both faces. The triage read them at ac8d523; the line numbers moved, the readings reproduce:

keybase.tsbase.zod.ts
visibleboolean | string :281union :158
hiddenboolean :328z.boolean() :175
disabledboolean | string :354union :190

The triage's claim that the Zod mirror is NOT already ahead of TS — re-measured, and it holds. Run against the unmodified base tree:

hidden: '<predicate>' safeParse.success = false
{ code: 'invalid_type', expected: 'boolean', path: ['hidden'] }
visible: '<predicate>' safeParse.success = true (lit control — the probe can pass)
hidden: true safeParse.success = true (control, legal in both states)

Both faces refused the string. The specific line the triage cited (base.zod.ts:170) is visibleOn on this base; hiddenOn sits at :180. Both are z.string(), so the citation had drifted by a line but the conclusion it supported is correct.

The docs row is hand-written, not generated — so the stop-condition on that row does not fire. Four readings, with a lit control on the one that could otherwise return a confident zero:

  • 15+ tracked files in this repo carry a real generation banner (git grep -liE 'DO NOT EDIT|@generated|automatically generated'); content/docs/api/schema-reference.md carries none. The control proves the probe fires.
  • git check-ignore does not ignore it — it is a tracked file.
  • No script writes to it; every script that names it (check-doc-component-types.mjs, check-doc-snippet-types.mjs) only reads it.
  • Its history is hand-authored docs PRs editing individual rows, e.g. ab7dc31ce "state every declared BaseSchema member at its declared type".

Reverse verification — direction predicted before running

Mutation: restore hidden to boolean-only on both faces, keep every new pin. Mutation and restore each proved on disk by anchor counts in both directions; the rebuilt dist was re-read each way so nothing was measured against a stale .d.ts; the restore was proved by state (git diff HEAD empty and both worktree blob hashes byte-identical to their HEAD blobs), never by an exit code.

Predicted RED, observed RED, no surprises:

sitepredictedobserved
base-schema-hidden-predicate.test.ts invariant Equal assertions (3)TS2344TS2344 at :90, :95, :98
the two authorable string fixturesTS2322TS2322 at :106, :112
the Zod-face runtime casevitest fail× zod mirror: a predicate string on 'hidden' parses in full
the two declared-path react call sitesTS2322TS2322 at :210, :213 — and those two were the only errors in the whole react test project

Predicted GREEN and observed GREEN in both states — the carriers are legal either way:

  • every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx, including the new declared-path one. A declaration change cannot move runtime behaviour, and this is the control that says so.
  • base-schema-visible-predicate.test.ts, and the visible / disabled control cases inside the new pin.
  • hidden: true / hidden: false parse, and hidden: 123 is refused. The refusal is the anti-overshoot guard: widening to z.any() would satisfy every positive case on its own.

One prediction I got wrong, and it was a NOT-MEASURED rather than a green. The first mutation run measured @object-ui/types and @object-ui/react in one recursive pnpm --filter invocation. It stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg did not run at all — reading its absence from the output as "react stayed green" would have been wrong in the most ordinary way. It was re-run on its own, under the same mutation, and produced the two predicted errors and nothing else.

⭐ One thing the ruling could not have known: the widening seeds a ledger entry

zod-mirror-parity.test.ts turned red naming app.zod.ts#AppComponentSchema — a pair with no ledger entry, so its recorded drift was never. Cause, measured on @objectstack/spec@17.2.0 by resolving AppSchema.shape at runtime:

  • the spec's AppSchema declares hidden (z.boolean().optional() — accepts a boolean, refuses a string) and declares neithervisiblenordisabled;
  • AppComponentSchema is BaseSchema.extend(SpecAppFields.shape).extend(...), and SpecAppFields excludes six keys with hidden not among them — so on the mirror face the spec's boolean lands after the base's and overrides it;
  • the TS interface extends BaseSchema and does not restate the key, so it inherits the widened union.

That asymmetry is the spec's, one layer under the one this card removed: visible and disabled widened without seeding anything precisely because the spec's AppSchema does not declare them. Attribution is measured, not assumed — under the mutation above, this error disappears.

Handled the way that file instructs a firing to be handled: by measuring, with a KnownDrift entry carrying the measurement and the reason. ReconcileAgainstLedger compares invariantly, so the file going green proves the entry is exactly the measured drift — the single key hidden, no more and no less. The ledger's prose counts moved with it (39/55 to 40/56, and the two "pairs with no entry" figures).

⛔ I did not close it by dropping hidden from SpecAppFields: that would make a spec-derived schema accept, by local divergence, a value the spec refuses — a second published surface, outside this ruling. The collision underneath (the spec's app-catalogue flag and the renderer's hide predicate sharing one name) is filed as objectui#7542, unassigned, with the directions measured and none chosen.

What was run, at f7b8948b1

runresult
pnpm build (whole repo)43 packages, 0 errors. This is also the consumer sweep — direction: dependents of @object-ui/types, all of them, green.
pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json)exit 0, 0 error TS lines
pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json)exit 0, 0 error TS lines
pnpm exec vitest run packages/types/97 files, 1642 tests, all passed
pnpm exec vitest run on 6 react visibility/enablement suites6 files, 105 tests, all passed
check:control-bytesOK, 6212 tracked text files scanned
check:doc-types / check:doc-fences / check:doc-snippetsall OK; doc-snippets judged 456 of 456 blocks against the built types
check:phantom-deps / check:published-tsconfig-exclude / check:readme-exportsall OK (readme-exports over the full population: 0 unbuilt, 52 keys compared)
check-changeset-presence / check-changeset-no-majorOK — 1 changeset declared, no major
check-governed-queue-guard --test over all 7 changed pathsNOT GOVERNED; the draft hold here is Clause-② only
pnpm lint (eslint . --no-inline-config, full repo, not narrowed)4257 files linted. My 5 changed source files: 0 errors, 0 new warnings. The repo carries 93 pre-existing errors across 77 files, none of them touched by this diff.

The test file the cast was dropped in is genuinely compiled: tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half of that pin is checked rather than merely written.

Not measured, declared: remote CI. Per the dispatch contract the report is handed over at draft-PR time and CI convergence is the reviewing seat's; nothing here waits on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

… evaluates
`hidden?: boolean` becomes `boolean | string`, and the Zod mirror's
`z.boolean()` becomes `z.union([z.boolean(), z.string()])` — matching `visible`
and `disabled` on both faces. `hidden` was the third key on the same evaluated
path and the only one still declared boolean-only.
The renderer never read this key as a boolean: `SchemaRenderer`'s `shouldHide`
chain asks `hasDeclaredPredicate(newSchema.hidden)` and then evaluates the
value. Measured before this change: a predicate string on `hidden` failed
`safeParse` with `invalid_type` at path `hidden`, while the identical string on
`visible` parsed.
Behaviour is unchanged — this is a declaration catching up with a shipped,
pinned capability. The docs row now states what the renderer does, the react
pin drives the string form through the declared path with no cast, and a new
types-side pin holds the three keys to one declared type.
The CEL envelope object form stays undeclared on all three keys; objectui#7530
rules on all three together.
Ruled by the maintainer on 2026-09-03, objectui#7455 option A.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… exposes
`AppComponentSchema` is `BaseSchema.extend(SpecAppFields.shape)`, and
`@objectstack/spec@17.2.0`'s `AppSchema` declares `hidden` (boolean-only) while
declaring neither `visible` nor `disabled`. So widening the base moved only the
DECLARED face of that pair, and `zod-mirror-parity` named it.
Seeded as a `KnownDrift` entry with the measurement and the reason, per that
file's own instruction to fix a firing by measuring. The collision underneath —
the spec's app-catalogue flag and the renderer's hide predicate sharing one name
— is a contract question, filed separately and not decided here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 50 chunks)3182.1 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-D0M_Mv2Z.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)516.19KB117.80KB
core (index.js)6.12KB2.42KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)242.42KB61.26KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (builtinAggregateLabels.js)0.86KB0.49KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.65KB1.47KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)34.34KB9.17KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)48.15KB13.35KB
plugin-charts (index.js)70.94KB19.75KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.89KB34.68KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.07KB64.12KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)167.46KB41.06KB
plugin-grid (index.js)209.25KB56.71KB
plugin-kanban (index.js)52.71KB14.55KB
plugin-list (index.js)113.33KB27.60KB
plugin-map (index.js)20.55KB6.80KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.57KB11.96KB
plugin-timeline (index.js)30.84KB8.85KB
plugin-tree (index.js)9.40KB3.23KB
plugin-view (index.js)85.22KB20.93KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)4.63KB2.18KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)4.58KB2.23KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

合约审查裁断:PASS —— 逐字采信

domain:ui 派发席,session session_01EMrWaQw3XS5DxTHxp4yRyC。本席是 opus、不在 CONTRACT_REVIEW_TIER。简报只含卡片、裁决与本 PR,⛔ 不含派发指令、⛔ 不含本席结论;并在初始简报里就写明了收口要求(未跑完的逐项进 NARROWINGS,每项须答「它本可能抓到本 diff 引入的什么」)——本轮前三次都是等审查者停在进度上才补发,这次前置了。

转录档位核验:只提取 harness 生成的 model 字段。转录 262 行、0 行不可解析、127 个 assistant 轮次全部 claude-fable-5-1,无 fallback ⇒ 逐字采信。


VERDICT: PASS

Reviewed: PR #7543, head f7b8948b1, base f0f774b0d. Card #7455 (body, all four comments incl. triage 5524392233 and ruling 5529434441), #7088, #4581, #4580, #3955, #7530, #7542, and ADR-0089 read in full.

What I measured, and what each ruled out

Claim 1 — the widening is exactly boolean | string on both faces and nothing else moved. Instrument: a TypeScript checker-API script listing every declared member of BaseSchema (TS face) and of the zod mirror's _input/_output on both trees, then diff — names, optionality, typeToString, index signatures listed separately. Result: the only differing member on all three listings is hidden?: boolean | undefinedhidden?: string | boolean | undefined; remaining diff lines are import-path text only. Second instrument: tsc builds on both trees, dist/**/*.d.ts compared with comments stripped — base.d.ts and zod/base.zod.d.ts differ only on the hidden line; the 14 other zod .d.ts files differ on inlined hidden members plus reorder-only lines (removed/added non-hidden lines identical as sorted multisets in every file). ⚠️ The whole-object Equal I first tried is unusable across two trees because the two BaseSchema interfaces are distinct symbols — that is why the checker listing is the instrument. Rules out: a narrowing, an any/deleted member hiding under the index signature, and any collateral member movement.

Claim 2 — the envelope form is declared nowhere.visible/disabled are exactly boolean | string | undefined on both faces at head; grep -iE 'ExpressionWire|dialect' in base.ts/base.zod.ts returns nothing. Runtime: { dialect:'cel', source:'true' } is REFUSED on hidden, visibleanddisabled (invalid_union at each path). The envelope pin is still at hiddenDeclaredGate.test.tsx:218 through its existing helper, unchanged.

Claim 3 — the docs row is hand-written.typedoc.json"out": "docs/api" (gitignored at .gitignore:47) — a different directory from tracked content/docs/api/; git check-ignore → not ignored; the only scripts naming the file read it and none writes it; no generation banner; history is row-level docs PRs. The ruling's stop-condition does not fire.check:doc-types, check:doc-fences, check:doc-snippets (456/456 against my own freshly built types) all green.

Claim 4 — the KnownDrift entry (the centre). Mechanism reproduced at runtime on the resolved @objectstack/spec@17.2.0: AppSchema.shape has hidden (accepts true, refuses a string) and has neither visible nor disabled; specFieldsExcept keeps every spec key not in its six-name omit list; head AppComponentSchema refuses hidden: '<predicate>' while accepting the same string on visible/disabled; dist app.zod.d.ts:236 reads z.ZodOptional<z.ZodOptional<z.ZodBoolean>>. Ledger exactness proven in both directions: deleting the entry → red (Type '"app.zod.ts#AppComponentSchema"' is not assignable to type 'never'); over-recording it as 'hidden' | 'visible' → same red; head → green ⇒ the measured drift is exactly {hidden}. Counts: 40 entries / 56 keys at head, 39 / 55 at base, matching the prose. Attribution: under the widening mutation the entry goes stale and parity reds the other way ⇒ the drift exists iff the widening exists. Disposition: the file's own rules ("fix it by MEASURING… correct its KnownDrift entry to the measured set") and its precedent make a reasoned entry the prescribed path; only UnmirroredDeclared is shrink-only, and this is type drift. Scoping the widening to avoid seeding would mean restating hidden?: boolean on the TS AppComponentSchema#7542's direction 1, a ruling on a second published surface the #7455 ruling did not authorise, and it would under-declare the renderer's predicate on the app node exactly the way #7455 corrects. I do not rule on #7542.

Claim 5 — red-first on the base. Base mirror: hidden: '${data.status === "draft"}'success:false, invalid_type expected boolean at ['hidden']; same string on visible → parses (lit control); hidden: true → parses (control legal in both states). Head: string parses, 123 refused. The triage's withdrawn "zod is already ahead" claim was correctly withdrawn.

Claim 6 — reverse verification. Mutation (both faces back to boolean, anchors 1/1 each way, dist rebuilt and re-read each way, restore proved by blob hashes equal to HEAD:): types leg red at :90/:95/:98 (TS2344) and :106/:112 (TS2322) exactly as predicted; vitest × zod mirror: a predicate string on 'hidden' parses in full with four controls green; react leg run alone → exactly two errors at hiddenDeclaredGate.test.tsx(210,66) and (213,46); react runtime pin green (27/27) under mutation; and the recursive pnpm --filter types --filter react type-checkreproduced ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL with the react leg never executing. ⭐ No other recursive-runner reading in the PR's evidence hides the same trap: the build is turbo (per-task verdicts; my forced rebuild 43/43), lint is a single eslint ., vitest runs are single invocations, and the &&-chained type-check scripts can only report the third leg if the first two passed.

Claim 7 — lint population.eslint . --no-inline-config -f json from the root on a quiet tree with probe files excluded: 4257 files, 93 errors across 77 files — the PR's figures exactly; the five changed source files 0 errors; none of the 77 error files is a path this diff touches.

Claim 8 — changeset.minor on @object-ui/types, one fixed group of 40; check-changeset-no-major / -presence / -fixed green. Level matches the ruling and AGENTS.md §版本号策略.

ADR-0089 / JSDoc honesty. The quoted sentence is verbatim from the ADR's TL;DR; its Decision section scopes packages/spec/src/** and D4 carves out the boolean keys. The JSDoc says the ADR governs packages/spec's keys not this surface, and that it "is evidence of intent about the same concept, which is why this widening was ruled rather than applied mechanically" — neither claiming consistency nor hiding the carve-out. Honest.

Consumer sweep and merge.Forced full build (turbo run build --force, 0 cached): 43/43 — my own compile, not the cache replay my first run turned out to be. packages/types/ 97 files / 1642 tests; seven react visibility suites 152 tests; react type-check exit 0. Merging current origin/main (moved by #7533, #7538) is conflict-free (main's schema-reference.md hunk is at 524–528, not 72), and on the merged tree the ledger, packages/types/ vitest (99 / 1661), the react hidden pin, check:doc-types and both changeset gates are green.

Non-blocking observations (no action required for the verdict)

  1. The PR body's "under the same mutation that error disappears" describes the un-ledgered tree. On the final tree the same mutation adds a sixth types-leg error — zod-mirror-parity.test.ts(1398,14), the entry going stale — which is the ratchet working and the stronger attribution; the reported red set is one line short, the code is right.
  2. zod-mirror-parity.test.ts's "160 pairs" is stale prose (pre-existing since fix(types): anchor ObjectGalleryProps / ObjectDataTableProps schema to exported schema types that extend BaseSchema (#6576, #6914) #7355): MIRRORS and Declared each have 154 keys on base and head. The PR carries "160 − 40 = 120" forward; the read figure is 154 − 40 = 114 (and 137, not 143, for "no entry in either"). Not introduced here — worth a docs-only follow-up rather than a rider.
  3. The hidden JSDoc quotes the evaluator as (condition: string | boolean | undefined, …); ExpressionEvaluator.ts:339 also lists { dialect?: string; source?: string }. The block acknowledges the envelope arm two paragraphs later, so nothing is concealed. The changeset names the author- and validator-side effects but not the one reader-side consequence (a boolean-typed read of schema.hidden must now narrow); the fix(types): BaseSchema.visible accepts the predicate string the renderer evaluates (#4581) #4593 precedent addressed that explicitly.

NARROWINGS

  • Remote CI convergence — not measured. Hosted checks are outside this seat; local equivalents plus a local merge with current origin/main were run. Could have caught: nothing beyond what the local ledger, build, lint and gates already exercised; the one merge-sensitive artefact (the parity ledger) was checked on the merged tree.
  • React type-check on the merged tree — not measured. It resolves @object-ui/* through built dist; main's two commits would force a second rebuild. Could have caught: a compile break in the two renderDeclaredNode call sites — main did not touch base.ts, base.zod.ts or the react pin, and the merged packages/types test project is green.
  • check:doc-snippets on the merged tree — not measured. Same rebuild cost. Could have caught: a doc snippet on main colliding with the widened hidden — main's docs change is the tree-view table at 524, unrelated.
  • Whether app-shell renders AppComponentSchema through shouldHide — not measured. It bears on 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's answer, which I am fenced from ruling. Could have caught: nothing this diff introduces.

派发席处置

**放行。**⚠️update_pull_request / enable_pr_auto_merge 自 19:26 起持续被 GitHub 配额拒绝(评论与标签端点仍可用),⇒ ready + auto-merge 待补,已排定时提醒

⭐ 本次按新立的次序执行:先 undraft + auto-merge、成功后再剥 needs:contract-review。所以标签此刻仍在——这是自洽状态(有标签、是草稿),而不是本轮早先两次先剥标签留下的「标签显示已复核、实际仍是草稿」。

观察 2 已记为独立跟进项zod-mirror-parity.test.ts 的「160 pairs」是 #7355 以来的既有陈旧散文(实读 154),⛔ 不作为本 PR 的搭车修改。它与本轮另外两例同属一类——散文陈述了一个不成立的事实且无 gate 覆盖——本席会在配额恢复后连同 safe-validate-corpus-6318.test.ts 头部那处一并立卡,⛔ 立卡前先做去重(本轮 search_issueslist_issues 都曾被限流,拿不到读数时连「零」都不是读数)。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 19:55
@os-project-manager
os-project-manager added this pull request to the merge queueSep 3, 2026
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

放行 — 合约复核 PASS,已 ready + auto-merge (SQUASH)

标签 needs:contract-review 已剥。⚠️ 次序说明:先 draft:false + auto-merge,成功之后才剥标签——反过来会在配额中断时留下「标签显示已复核、实际仍是草稿」的不自洽状态。

一处记录(⛔ 不据此扣住本 PR,不要求修改)

本 PR 编辑了 packages/types/src/__tests__/zod-mirror-parity.test.ts 的表头计数。你动到的那个数是对的——KnownDrift 39/55 → 40/56,我在你的 head f7b8948b1 上独立数过,实测就是 40 条 / 56 键

但表头的 MIRRORS 基数是错的,而本 PR 把它代进了一条新算式

表头行本 PR head 上写的在同一 head 上实测
:93160 − 40 = **120** ← 本 PR 新写120114(154 − 40)
:56**160 pairs**160154
:31 / :1459all 160 entries160154
:91 — “stands at 143143137

154 用三种互不相同的数法各数一次(2 空格缩进带引号键、'*.zod.ts#Name': 行匹配、去重键集合),三者一致,所以不是数法造成的差。基数变成 154 是 d88e20f55#7432 退役 block schema 家族,−9 对),发生在本 PR 分叉之前。

这不是对本 PR 的指摘。160 是继承来的,不是你写坏的;而且更值得记的是往回追的结果——160 这个数写下当天就是假的:把表头写的值与同一提交上的实测值逐个比对,引入 1604ca30d044(2026-09-02)当时 MIRRORS 实际是 163

提交日期表头写实测
7797e3b7408-24158✅ 158
2c1c9673508-30158✅ 158
a04d7c65c08-30158159
2c45966ff08-31158161
a8d5c717e09-02158161
4ca30d04409-02160 ← 在此写入163
d88e20f5509-03160154

所以这不是「表头烂了、重写一遍就好」。#6141 已经用「重写一遍」补过一次——它加的那段小标题正是「每个数字是怎么来的」,开头一句是「⚠️ 本表头每个计数都是出来的,不是继承的」,理由是此前三个数(两次「163 pairs」、一次「13 entries」)已经陈旧、并被 #5927#6058 当作漂移问题的规模引用过。九天内又烂了,而且是在重写的那一刻就烂的。

表头自己也把原因写明了:「Nothing asserts it against a written number, so this line is prose and can rot」。

⇒ 我会把它与另一处同类(examples/schema-catalog/test/safe-validate-corpus-6318.test.ts 的表头把已被 #6939 修好的 tooltip / context-menu / tree-view 仍列为 open findings)合并成一张卡,把「这类计数该派生、该上 gate、还是该删掉」作为待裁的分叉交给维护者。⛔ 我不在本 PR 里手改 120 → 114:手改正是这张卡断言撑不住的那个补救法,改完下一次落地照旧。


Generated by Claude Code

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

Projects

None yet

2 participants

@os-project-manager@claude