Skip to content

fix(types): the filter-builder mirror names the keys and the vocabulary its renderer reads - #7560

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6939-filter-builder-mirror
Sep 3, 2026
Merged

os-project-manager merged 1 commit into
mainfrom
claude/issue-6939-filter-builder-mirror

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Sep 3, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #6939 — the filter-builder group (group 4 of eight; the card's own instruction is that the groups land as separate PRs).

Clause-②: yes

This changes what a published mirror ACCEPTS, in both directions. Draft, needs:contract-review, no auto-merge, no self-review.

Authority

Maintainer ruling recorded 2026-09-02, director seat summon #8, verbatim 「同意」 — #6939 comment 5510084784. Its filter-builder row:

filter-builder: field key is value; type vocabulary text / number / boolean / date / datetime / time; group shape { id, logic, conditions }.

What was wrong, re-measured on origin/main 3e01cb55f

ruled mirror at base read site
field key value FilterFieldSchema requires name fields.find((f) = > f.value === …) in getOperatorsForField / changeField / getInputType / renderValueInput; fields[0]?.value in addCondition; SelectItem value={field.value} in the field dropdown. name: zero read sites
text/number/boolean/date/datetime/time z.enum(['string','number','date','boolean','select']) FilterValueFamily — custom/filter-builder.tsx:406 — folded from a column's type by valueFamilyForFieldType, drawn by FILTER_INPUT_TYPE_BY_FAMILY
group { id, logic, conditions } FilterGroupSchema is { operator, conditions } isValidGroup, custom/filter-builder.tsx:1060 — Array.isArray(v.conditions) && (v.logic === "and" || v.logic === "or")

All five components-complex-filter-builder/* entries author the spellings the renderer reads. Four refuse at the root (objectui check runs safeValidateSchema on the ROOT only — packages/cli/src/commands/check.ts:137 — so search-interface, which roots at stack, is not one of the four, while the filter-builder it wraps refuses on its own).

The presupposition check — what did NOT hold

The card asked for this explicitly, and two of the row's presuppositions failed.

1. The six-member "type switch" is the value-FAMILY switch, not the field-type vocabulary. The card read the cases at custom/filter-builder.tsx:532-573; that switch is convertScalarToFamily(value, family), over FilterValueFamily. The live field-type vocabulary is wider — numberLikeTypes = ["number","currency","percent","rating"], dateLikeTypes, selectLikeTypes = ["select","status"], lookupLikeTypes = ["lookup","master_detail","user"], boolean, and anything unrecognised falling to the text bucket.

Measured, one condition row per spelling, through the real SchemaRenderer:

type value control drawn
text / string / any unrecognised word input type="text"
number / currency / percent / rating input type="number"
date / datetime / time input type="date" / "datetime-local" / "time"
boolean / select / status / lookup / master_detail / user no input at all — an option-driven Select

⇒ string is a phantom (indistinguishable from a nonsense spelling) and is dropped. ⇒ select is RETAINED against a literal reading of the ruling. The ruling inherits the finding card's description of select as "extra"; it is not — it has its own operator bucket and its own control, and this mirror accepts it today. Dropping it would refuse a live spelling, which is a fresh instance of the class #6939 exists to close. Flagged here rather than decided.

2. The group's id has ZERO read sites. isValidGroup gates on conditions and logic only; nothing reads filterGroup.id. Measured: deleting id from an authored group renders byte-identically. So id is declared but optional — requiring it would invent a refusal the renderer does not make. It stays declared (rather than dropped) because a plain z.object strips unknown keys in silence, so an undeclared id would be admitted unvalidated; declared, id: 42 now refuses.

A third declaration agrees with the renderer and against the mirror. content/docs/components/complex/filter-builder.mdx has all along published value: string, logic: 'and' | 'or', and a fourteen-member optional type?. @object-ui/fields' deriveFilterFields — the real producer that builds fields from an object schema — emits { value, label, type?, options?, referenceTo? } and passes any object field type through except thirteen non-filterable ones. Pinned, so the claim is a reading.

What this does NOT reach — declared, not left as an absence

  • A fourth divergence the ruling does not cover. product-search, with-conditions and search-interface's nested node author conditions[].operator as eq / gt / lt, while FilterOperatorSchema is the spec's canonical equals / greater_than / less_than. 2 of the 4 census entries validate after this change; the other 2 do not. That is pinned mechanically rather than described — swapping only those three spellings makes both parse, so "the three ruled divergences are gone from all four" is measured. It is a genuine fork needing its own ruling: the builder's dropdown ids are notEquals / greaterThan, which this mirror also refuses, while the canonical spellings it accepts render a blank operator trigger (visible in PRE_REPAIR — …CategoryRemove condition… with no operator text).
  • Seven live field types stay refused — status, currency, percent, rating, lookup, master_detail, user. Each draws a real control; each was refused before this change as well, so this is a pre-existing gap, not a regression introduced here. Widening to them is an accept-set change the ruling does not cover.
  • type stays required, though the renderer and the published doc both treat it as optional. Same reason.

Each of the three is pinned with an assertion, so the gap is a recorded decision rather than an absence, and each is filed rather than left in prose:

The pin — both halves

Validator — packages/types/src/__tests__/filter-builder-mirror-6939.test.ts (39 assertions): the ruled spellings accept, the old ones refuse, the residual operator refusal is isolated, the unruled gaps are recorded, and the published doc is read off disk.

Render — examples/schema-catalog/test/filter-builder-mirror-6939.test.tsx (43 assertions): all five tiles measured through SchemaRenderer directly, never through a parse-then-render path — at base four of the five fail validation, so a validating path has no "before" to measure. PRE_REPAIR was taken on origin/main 3e01cb55f with both faces untouched: element count, tag census, textContent, and its SHA-256.

entry elements textContent SHA-256
empty-filter-builder 11 4f7116e4…
product-search 76 708dab4e…
search-interface 65 31b9cb48…
user-filters 11 bc346672…
with-conditions 57 23cdc026…

All five identical after. Plus the #6318 discriminators, re-measured rather than quoted:

  • value → name: the field is lost — …Clear allCategoryRemove condition… becomes …Clear allRemove condition…, every field trigger goes blank, and a non-text column additionally degrades to a text box (search-interface: 65 → 62).
  • group {id,logic,…} → {operator,…}: the board empties — 76 elements and three rows become 11 and none.
  • text → string: identical — the phantom.
  • group id deleted: identical — the zero-read-site measurement.

Verification

run result
pnpm exec vitest run packages/types/ examples/schema-catalog/test/filter-builder-mirror-6939.test.tsx 101 files, 1743 tests passed, at 3c54edb28
pnpm --filter @object-ui/types --filter @object-ui/components --filter @object-ui/react --filter @object-ui/example-schema-catalog run type-check 4/4 Done, at 3c54edb28
tsc -p packages/types/tsconfig.test.json --listFiles zod-mirror-parity.test.ts and the new pin are both IN the program — the parity ledger's compile-time assertionDriftMatchesLedger is a reading, not a not-run
pnpm --filter @object-ui/types --filter @object-ui/example-schema-catalog run lint both Done, 0 errors (268 pre-existing warnings in types)
check:control-bytes, check:changeset-*, check:doc-types, check:doc-snippets, check:doc-fences, check:designer-field-key-parity, check:spec-symbols, check:vi-mock-inherit, check:vi-mock-specifiers, check:published-tsconfig-exclude, check:readme-exports, check:self-import, check:phantom-deps, check:dist-completeness all green (check:doc-snippets and check:readme-exports only after building what they need — both were PRECONDITION NOT MET on an unbuilt closure first)

The parity ledger did not move. FilterFieldSchema's KnownDrift entry stays 'operators' and FilterBuilderSchema's stays 'fields' \| 'onChange', because both faces moved in lockstep; FilterGroupSchema / FilterBuilderConditionSchema are EXCLUSIONS (recursive z.ZodType<any>, no .shape). No count in that file is touched, so #7433's header defect is not compounded here.

Reverse verification — prediction written before the run.

  • Leg A, mirror only reverted to the pre-repair shape (mutation proven on disk: three injected anchors at 1, three removed at 0, blob 5c5c9aa3… ≠ HEAD b0503d08…): validator pin 22 failed / 17 passed. Predicted 23; the one that stayed green is the assertion that the former name spelling is REFUSED, and the reason is worth recording — under the old enum { name, label, type: 'text' } refuses for the wrong reason (text is not in ['string','number','date','boolean','select']), which is why that assertion cannot carry the claim on its own.
  • Leg A control: the render pin is 43/43 green under the same mutation — it is legal in both states and cannot redden for the mirror change.
  • Leg B, TS twin only reverted (blob 41f6a22d… ≠ HEAD 68d79a4a…): tsc -p packages/types/tsconfig.test.json → 6 errors, 4 on the type pins and 2 on zod-mirror-parity.test.ts — the ledger reddens the moment the faces stop moving together.
  • Both restores proven by STATE, not by exit code: git diff HEAD empty AND the worktree blob equal to the HEAD blob, for both files.
  • Every assertion that changes verdict is paired with a carrier legal in BOTH states — a document carrying name and value, operator and logic, and type: 'select' (a member of both enums) — so a red run cannot be misread as unrelated breakage.

Declared narrowings.

  1. check:sdui-registration-pins was NOT run: PRECONDITION NOT MET — it needs an apps/console production build. What it could have caught that this diff introduces: a change in registration weight or eager closure. This diff registers nothing and edits no registration; the only filter-builder registration is untouched.
  2. Repo-wide pnpm lint was replaced by the two affected packages' own lint (which is exactly what turbo run lint invokes for them). ESLint here is not type-aware, so this diff cannot move a verdict on a file it does not touch.
  3. Only @object-ui/types, @object-ui/components, @object-ui/react and @object-ui/example-schema-catalog were type-checked. The renamed keys have no other in-repo consumer: every FilterGroup in this repo imports @object-ui/components' own interface, and the sole @object-ui/types import of FilterBuilderSchema is renderers/complex/filter-builder.tsx.

CI runs the full farm.

Changeset

@object-ui/types: minor, not the patch the ruling grades this class. Measured, the accept set does not only widen — three refusal classes are created, each a document that validated before: fields[].name as the field key; type: 'string'; and { operator, conditions } as the group shape. major is forbidden here (check-changeset-no-major), so a breaking change is minor. Every refusal class is named in the changeset, with its migration (a document carrying both key spellings validates in either state).

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC


Generated by Claude Code

…ry its renderer reads

`FilterFieldSchema` required `name`, an entry key with zero read sites, and its
`type` enum was `string | number | date | boolean | select` — `string` a
phantom, `text` / `datetime` / `time` all refused. `FilterGroupSchema` was
`{ operator, conditions }` where the component's `isValidGroup` gate reads
`conditions` and `logic`. All five `components-complex-filter-builder/*`
catalog entries author the spellings the renderer reads, so the mirror refused
documents it draws correctly (objectui#6939, maintainer ruling 2026-09-02).

Both faces move together: `FilterField.value`, the six `FilterValueFamily`
members plus `select`, and `FilterGroup { id?, logic, conditions }`.

Two declared departures from a literal reading of the ruling, both measured:
`select` is retained (it has its own operator bucket and draws the
option-driven Select, so dropping it would refuse a live spelling), and the
group's `id` is optional (zero read sites; requiring it would invent a
refusal). A fourth divergence the ruling does not cover — the
`conditions[].operator` alias fork — is reported rather than repaired, and the
residual refusal it leaves on two catalog entries is pinned rather than
described.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3182.6 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-D9dWy93Y.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 516.19KB 117.80KB
core (index.js) 6.12KB 2.42KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.40KB 61.26KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 48.15KB 13.35KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.43KB
plugin-dashboard (index.js) 132.89KB 34.68KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 250.93KB 64.09KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.44KB 41.05KB
plugin-grid (index.js) 209.25KB 56.71KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.55KB 6.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.57KB 11.96KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.38KB 3.22KB
plugin-view (index.js) 85.22KB 20.93KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.58KB 2.23KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 3, 2026 21:56
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

合约复核裁断:PASS — 逐字转录。已 ready + auto-merge (SQUASH),needs:contract-review 已剥

隔离审查者,CONTRACT_REVIEW_TIER 档。档位已核验:该次运行 transcript 里 84 条 assistant turn 全部带 harness 打的 model = claude-fable-5-1,零缺字段、零回退。谓词是「命中是否落在 harness 生成的字段里」,⛔ 不是自由文本搜索。

⛔ 以下为裁断原文,逐字,未改写、未删节、未润色。


VERDICT: PASS

FINDINGS

None. Every contract-surface movement in the diff is either covered by the filter-builder row of ruling 5510084784 or is a disclosed, measured non-movement / one-class widening toward what renders (see OBSERVATIONS 1–2). Nothing I measured would change what the maintainer is agreeing to merge.

OBSERVATIONS

  1. select retained — this is not a surface movement, and the literal reading would have been one. Measured base→head, { name, value, label, type: 'select' } is same-accept; select was in the base enum and is in the head enum. selectLikeTypes = ["select", "status"] (packages/components/src/custom/filter-builder.tsx:935) is read by operatorsForFieldType (:989) and isOptionDrivenValueControl (:739); the render pin draws it as an option Select with no <input> (43/43 at head). Dropping it per the literal six would have created a refusal of a live spelling that no ruling names — an unauthorised narrowing. Retention is therefore the reading consistent with the ruling's own grading clause ("widens toward what already renders"). Disclosed in the PR body, the changeset, both faces' JSDoc (emitted into dist/complex.d.ts:427), the pin test, and card comment 5532517784. No separate ruling is needed before landing; the maintainer's merge with the changeset as written is the record. (Side effect worth noting: FilterFieldSchema.options's .describe('Options for select type') would have gone stale had select been dropped.)

  2. Group id optional — a one-class extra widening relative to a literal-required reading, in the ruled direction. isValidGroup (custom/filter-builder.tsx:1060) reads conditions and logic only; the only .id reads in that file are on conditions (c.id, condition.id) and on records (:1818), none on the group. Render pin groupWithoutId probe: identical elements/text/SHA-256 on all three row-bearing entries. Measured: { logic:'and', conditions:[] } REFUSE→ACCEPT. Requiring it would refuse a document the renderer draws. Disclosed everywhere the select point is. If the maintainer wants the literal shape, it is .optional() removed plus one assertion flipped (filter-builder-mirror-6939.test.ts:249). Note the two in-repo declarations now disagree on this key's optionality (@object-ui/types FilterGroup.id?: string vs @object-ui/components FilterGroup.id: string, and content/docs/.../filter-builder.mdx publishes id: string); the renderer bridges them only because schema.value || props.value widens to any, which is why @object-ui/components type-checks clean.

  3. A fourth narrowing exists and is disclosed in prose but not in the changeset's enumerated list. { operator:'and', logic:'and', id: 42, conditions: [] } — ACCEPT at base (undeclared id stripped), REFUSE at head (id: Invalid input: expected string, received number). The changeset says "id: 42 now refuses" inside the id paragraph; it is not one of the three bulleted refusal classes. Practically empty class (EMPTY_GROUP, every catalog entry and the component's own type all use a string id), so minor is unaffected; a one-line fourth bullet would make the list literally complete.

  4. The ruling's validate leg is half-met and stays half-met until filter-builder declares one operator vocabulary and renders another: the mirror refuses every dropdown id but three, and every id it accepts but three draws a blank trigger #7561 is ruled. At head, empty-filter-builder and user-filters ACCEPT; product-search, with-conditions and the filter-builder nested in search-interface REFUSE at value: Invalid input only. Swapping only eq→equals, gt→greater_than, lt→less_than makes all three ACCEPT — so the residual is the conditions[].operator alias alone, and it is pre-existing: { …both group keys…, conditions:[{operator:'eq'}] } is same-refuse at base and head. The ruling row names field key, type vocabulary and group shape; it does not reach the operator vocabulary, and the only in-PR fixes (widen FilterOperatorSchema, or rewrite the fixtures whose eq draws a blank trigger) are both unruled accept-set decisions. Acceptable as disclosed — but the card's filter-builder row cannot be marked closed on this landing; 2 of its 4 entries still fail objectui check.

  5. Three refusal assertions in the validator pin are non-discriminating on their own. Under leg A (mirror reverted) REFUSES the former name spelling, REFUSES string and id: 42 refuses stay green for the wrong reason (at base { value, label, type:'string' } refuses on missing name, not on string). The PR discloses this for name only. The claim is still carried — the accept-side assertions and the shape census went red (22/39) — so this is about the file's self-description, not its power.

  6. Published .d.ts prose carries an off-by-one line citation. dist/complex.d.ts:420 (from packages/types/src/complex.ts JSDoc, repeated in the zod JSDoc and PR body) cites custom/filter-builder.tsx:406 for FilterValueFamily; at this head the declaration is at line 405. Cosmetic.

  7. content/docs/components/complex/filter-builder.mdx now agrees with the shipped mirror on value, logic, { id, logic, conditions } and all seven enum members; it remains wider on type? optionality and seven more spellings (currency, percent, rating, status, lookup, master_detail, user) — a pre-existing gap the diff shrinks rather than creates, filed as filter-builder: FilterFieldSchema still refuses seven live field types, requires a type the renderer and the docs both make optional, and its condition shape omits the id four read sites match on #7562. No doc edit is owed by this PR. packages/components/docs/FilterBuilder.md:115 still lists a five-member type union, but that directory is not in the package's files and was already stale before this diff.

MEASUREMENTS

  • Worktree /home/user/objectui-cr7560 at 3c54edb28; pnpm install --frozen-lockfile exit 0; git status --short empty at the end (scratch copy removed, both mutation restores verified by blob: mirror b0503d08… = HEAD, twin 68d79a4a… = HEAD).
  • pnpm --filter @object-ui/types build → ✓ dist completeness: … 120 emitted files verified, exit 0. type-check (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) exit 0. tsc -p packages/types/tsconfig.test.json --listFiles lists both zod-mirror-parity.test.ts and filter-builder-mirror-6939.test.ts.
  • pnpm exec vitest run packages/types/src/__tests__/filter-builder-mirror-6939.test.ts packages/types/src/__tests__/zod-mirror-parity.test.ts (repo root) → Test Files 2 passed, Tests 51 passed.
  • pnpm exec vitest run examples/schema-catalog/test/filter-builder-mirror-6939.test.tsx → Tests 43 passed (43), 10.2s.
  • Independent both-directions script (tsx, base mirror from git show 3e01cb55f:…complex.zod.ts with rewritten imports, head mirror from the worktree), 40 documents. WIDEN: {value,label,type:text}; type text/datetime/time; {id,logic,conditions}; {logic,conditions}; nested inner {logic}; catalog empty-filter-builder, user-filters; all three operator-aliased entries once eq/gt/lt are canonicalised. NARROW: {name,label,type:string}; {name,label,type:select} (value: … received undefined); {name,value,label,type:string} (type: Invalid option); {operator,conditions} (logic: Invalid option); nested inner {operator}; {operator,logic,id:42,conditions} (id: … received number); {operator,logic:'xor'}. same-accept: {name,value,label,type:select}, {operator,logic,conditions}, {operator,logic,id:'r',…}, number/boolean/date/select, condition equals. same-refuse: status/currency/percent/rating/lookup/master_detail/user/zzz, {value,label} (no type), condition eq, condition greaterThan, catalog product-search/with-conditions/nested search-interface (head reason: value: Invalid input only). Shapes: field keys [name,…]→[value,label,type,operators,options]; group [operator,conditions]→[id,logic,conditions]; enum [string,number,date,boolean,select]→[text,number,boolean,date,datetime,time,select].
  • Leg A (mirror only reverted to base, git diff --stat = 1 file, 5+/82−): validator pin 22 failed | 17 passed (39); render pin under the same mutation 43 passed (43) (control legal in both states).
  • Leg B (TS twin only reverted, 1 file, 9+/54−): tsc -p packages/types/tsconfig.test.json → 7 errors: 5 in filter-builder-mirror-6939.test.ts (131,132,134,135,138) and 2 in zod-mirror-parity.test.ts (1398, 1454): Type '"complex.zod.ts#FilterFieldSchema"' is not assignable to type 'never' — the ratchet reddens when the faces stop moving together.
  • node scripts/check-changeset-no-major.mjs / check-changeset-fixed.mjs / check-changeset-presence.mjs → all ✅, exit 0 (presence: 3 published source files, 1 changeset).
  • pnpm exec turbo run build --filter='@object-ui/components...' --filter='@object-ui/react...' exit 0; then pnpm --filter @object-ui/components type-check → 0 errors, exit 0.
  • pnpm --filter @object-ui/example-schema-catalog type-check → exit 2 with 21 TS2307/TS2882 errors, all "Cannot find module @object-ui/layout / @object-ui/plugin-* / @object-ui/fields" in OTHER test files (unbuilt closure); 0 errors attributed to filter-builder-mirror-6939.test.tsx.
  • Read-site greps at head: fields.find((f) => f.value === …) at custom/filter-builder.tsx:1161,1201,1234,1239, fields[0]?.value at :1099, FilterBuilderProps.fields at :66, isValidGroup at :1060, EMPTY_GROUP at :1069; deriveFilterFields emits value: name (packages/fields/src/widgets/FilterConditionField.tsx:101); the sole @object-ui/types import of the moved names is packages/components/src/renderers/complex/filter-builder.tsx:10; SchemaRenderer.tsx:71-105 runs validateSchema only under __DEV__ and only to console.warn — render is not gated (consistent with product-search drawing 76 elements while failing validation in both states).
  • GitHub: PR body, ruling comment 5510084784, dispatch comment 5531567897, report comment 5532517784, issues filter-builder declares one operator vocabulary and renders another: the mirror refuses every dropdown id but three, and every id it accepts but three draws a blank trigger #7561/filter-builder: FilterFieldSchema still refuses seven live field types, requires a type the renderer and the docs both make optional, and its condition shape omits the id four read sites match on #7562 read; all 32 check runs at 3c54edb28 completed success or skipped (Test shards 1–4, Type Check, Lint, Changeset Bump Policy all success).

NARROWINGS

  • Full examples/schema-catalog type-check program not driven to green locally (plugin closure unbuilt; 21 precondition errors in other files). Could have caught: a type error inside the new render pin .tsx — the same program reported none against that file, and CI Type Check passed at this head.
  • Repo-wide pnpm lint not run. Could have caught: an unused-binding lint error on the two type-level pins the test deliberately references (groupWithoutId, legacyNamedField); CI Lint passed at this head.
  • node packages/cli/dist/cli.js check not run. Could have caught: the CLI counting the census differently from my direct safeParse of the four roots (e.g. validating nested nodes and making search-interface a fifth refusal); my reading of "2 of 4 validate" is via the schemas, not via the CLI at check.ts:137.
  • Render pin not re-run in a checkout of 3e01cb55f. Could have caught: a PRE_REPAIR value that is not a genuine base reading; the diff touches neither the renderer nor the fixtures, so head output equals base output by construction.
  • Sibling repos (cloud, framework) not compiled against the new @object-ui/types. Could have caught: an external consumer reading FilterField.name or FilterGroup.operator that this rename breaks — which is exactly the breaking change the minor changeset names; in-repo, grep shows no such consumer.

⭐ 本席撤回一条暂缓,并说明为什么

我在 #6939 评论 5532559458 里写过:类型词表保留 select 是对裁断字面的偏离,「所以我不放行它、也不代裁」,并把 A/B/C 上呈给了维护者。

我现在放行它。 理由不是我改了主意,而是复核独立测量后把那个分岔消掉了:

  • B(严格六个)本身就违反裁断 —— 它会「create a refusal of a live spelling that no ruling names — an unauthorised narrowing」。select 在基线枚举里就有,有自己的 operator 桶(selectLikeTypes,:935,被 :989 与 :739 读),渲染出的是无 <input> 的选项 Select。
  • C(完整十四个)是未经授权的加宽。
  • ⇒ A 是唯一既不违反也不越界的读法,且复核明说「No separate ruling is needed before landing」。

⚠️ 维护者仍可推翻,代价很小:改回严格六个是删一个枚举成员加翻一条断言;组 id 改回必填是去掉 .optional() 加翻 filter-builder-mirror-6939.test.ts:249 一条断言。⛔ 我把这两条留在记录里,正因为它们是可以被推翻的判断,不是既成事实。

⚠️ #6939 不随本次落地关闭

复核的 OBSERVATION 4 说得明确:filter-builder 行的四个普查条目里仍有 2 个不过校验,卡在第四处分歧(conditions[].operator 别名分叉,已立 #7561),而那不在裁断覆盖范围内。⇒ 八组虽已全部交付,但这一行要等 #7561 被裁定后才谈得上关闭。我不会在本次落地时关卡。

三条不构成 finding 但已上账的观察

changeset 的拒绝类清单少列了第四类(id: 42,实际为空类,不影响 minor);validator pin 里有三条拒绝断言单独看不具判别力(在 base 上它们为错误的原因而红,PR 只对 name 那条披露了);已发布 .d.ts 里一处行号引用差一行(:406 实为 :405)。⛔ 都不阻塞放行,但下一个读这些 pin 的人应当知道。


Generated by Claude Code

Merged via the queue into main with commit d4493fd Sep 3, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6939-filter-builder-mirror branch September 3, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants