Repository navigation
feat(spec)!: retire the list view's filterableFields — declared as the userFilters.fields shorthand, read as a Filter-panel whitelist; no per-view whitelist replaces it - #24721
Conversation
…ter-panel whitelist replaces it The key was declared as the userFilters.fields shorthand while its one renderer read it as the Filter panel's field whitelist. Ruled C: the key retires with a retiredKey() tombstone on the list-view shape, the Studio form input goes, and a D2 conversion DROPS it from stored and authored view sources (never a rewrite to userFilters.fields), with its D3 entry, RETIRED_KEYS_BY_MAJOR rows, step-18 rationale and liveness row. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ss counts for the filterableFields retirement Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ithout the retired filterableFields input Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…673 with the retired filterableFields input Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…tire-filterable-fields
Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 770c2759b7f3cff12e3718703e084d83527d3eff && git checkout 770c2759b7f3cff12e3718703e084d83527d3eff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 54159fd742e5a79fc10ac41854dba3395e1a1ffe bb71914fc92a679303b28361f1cd9384bb9baa30 && git checkout -B drift-repro 54159fd742e5a79fc10ac41854dba3395e1a1ffe && git merge --no-ff bb71914fc92a679303b28361f1cd9384bb9baa30
node scripts/docs-audit/affected-docs.mjs --json 54159fd742e5a79fc10ac41854dba3395e1a1ffe
|
Contract reviewServed-tier: Read against the merge base ① Derived judgmentsD1 — ruling C and no wider: RIGHT. The diff removes exactly one authorable key, D2 — the prescription is true: RIGHT. It names D3 — a DROP, never a rewrite: RIGHT. D4 — the conversion metadata is right by the registry's own rules: RIGHT. D5 — the D3 entry and the retired-keys entries are true: RIGHT. D6 — the coverage boundary is honest and acceptable, with one stated cost: RIGHT. D7 — generated artefacts are regenerated and consistent: RIGHT. D8 — the pins hold the change: RIGHT. Per door: the refusal with issue code D9 — the RESIDUE assertion is right, with its cost named. The assertion is the last loop of the test D10 — changeset body: RIGHT. D11 — out-of-lane paths: RIGHT and minimal. ② Semver level
③ Boundary flagsF1 — the dev's deviations (a) to (f): accepted. (a) The JOIN row, the rationale fragment, the F2 — ADR-0047 Decision 1 is falsified by this PR; a governed follow-up, not a blocker. Confirmed at F3 — finding (1), the in-repo readers: reachable on one path, not dead; no duplicate beside the tombstone; follow-up. F4 — finding (2), the react F5 — finding (3), the objectui follow-up sites: confirmed at F6 — the check-runs on this head, read at 2026-10-20T02:20Z. Completed success: F7 — governed surfaces: none touched. No path under Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Contract review adopted ·
Generated by Claude Code |
ADR-0047's Decision 1 (section 3.3 item 1) still said `filterableFields` "stays as the legacy shorthand; loaders auto-upgrade it to `userFilters.fields` entries". The maintainer's ruling C on #24640 retired the key in 18.0 with no replacement key, and PR #24721 (4bc3bec) executed it: a retiredKey() tombstone refuses it at parse, and the conversion `view-list-filterable-fields-removed` drops it rather than rewriting it to `userFilters.fields`. The auto-upgrade loader the sentence promised was never built. Per Prime Directive 13 the record is amended in place: a dated Amendment (2026-10-20) blockquote joins the status block's existing Revision and Amendment chain, and Decision 1's sentence is struck with an annotation pointing at it, not deleted. No other ADR text moves. Claude-Session: https://claude.ai/code/session_01HEdP5tPhqQfzwRmKDxwn85 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #24640
Clause-②: yes (narrowing)
What
filterableFieldsretires from the list-view shape under ADR-0049 enforce-or-remove, on the maintainer's ruling C (record 6278975049, verbatim 「C」), pulled into 18.0 (record 6279033298, verbatim 「提前」). The key carried two meanings. The spec described it as the legacy shorthand foruserFilters.fields(the quick-filter bar), and the Studio view form offered it as "Field names available for filtering". Its one renderer, objectui'sListView(thefilterFieldsmemo), read it as the Filter panel's field whitelist. The ruling is that the platform carries no per-view Filter-panel whitelist, so no key replaces it.filterableFields: retiredKey(LIST_VIEW_FILTERABLE_FIELDS_RETIRED)sits inListViewShapeSchema, besidesearchableFields. Every door built from the shape refuses the key with the prescription:tsctypes itnever, and the parse raises the prescription instead of a bare unknown-key report. The prescription says: delete the key, no per-view Filter-panel whitelist replaces it; for a curated end-user filter bar declareuserFilters: { element: 'dropdown', fields: [{ field: 'FIELD_NAME' }] }; the Filter panel offers every field the user may read (field read security decides, and a field the object markshidden: trueis not offered). It closes with the houseos migrate meta --from 17sentence.filterableFieldsinput leavesview.form.ts, with a comment in its place. The four extracted form-label bundles are regenerated (pure deletion).view-list-filterable-fields-removed(protocol 18,retiredFromLoadPath: true,retiredAfter: '17.7.0') drops the key from every list payload instack.views[], in all three persisted spellings. It never rewrites it touserFilters.fields. The D3 entry islist-view-filterable-fields-retired.RETIRED_KEYS_BY_MAJOR[18]gainsui/ListView:filterableFieldsandui/ObjectListView:filterableFields. The step-18 rationale gains one fragment.view.jsonrow/props/list/children/filterableFieldsflipslivetodead. It keeps the tombstone discipline (verifiedAt2026-10-19,evidenceScope: cross-repo) and carries a REMOVED note naming the one reader the ruling makes unreachable.state-counts/view.mdis regenerated: live 79 to 78, dead 11 to 12.os:checkexample oncontent/docs/concepts/architecture.mdxstops teaching the key. It declares auserFiltersdropdown onindustryinstead, because the example meant a user-facing filter. The reference pages are regenerated..changeset/24640-list-view-filterable-fields-retired.md:@objectstack/specminor, with a BREAKING banner, a FROM to TO table, the one-line fix and a stored-row paragraph. It carries the Clause-② line and the ADR-0087 markerregistered view-list-filterable-fields-removed, list-view-filterable-fields-retired. Release arithmetic, measured:.changeset/pre.jsonis{ "mode": "exit", "tag": "next" }and the spec label is18.0.0-next.5. A minor increment of that prerelease is18.0.0, so the next version pass publishes this in 18.0.0.Premises, each with its reading (base
bfc1539fe0; re-read after mergingorigin/main1c1144fcd8)P1 holds. One declaration exists:
packages/spec/src/ui/view.zod.ts(was:2844) onListViewShapeSchema.ObjectListViewSchemaisListViewShapeSchema.omit({ userFilters }).extend(...), so one tombstone covers both. Bothauthorable-surface/ui.jsonrows (:757,:944) now read[RETIRED]. Insidepackages/spec/src,git grep -c filterableFieldsat base hit 4 files: the declaration, the form row and two pins. No schema outside the list-view family declares the key. The other carriers are the same family. The flattened overlay arms are built from the shape.ListViewPageProps(a pagelist-viewnode,ComponentPropsMap['list-view']) spreadsListViewSchema.shape. The reactListViewblock's schema isListViewSchema, and its curateddataPropsnever listed the key (see the Acceptance notes for the react-page boundary).P2 holds, and the census found more readers than the card listed.
Zero authors, each with a lit control on the same instrument at base.
git grep -cforfilterableFields, thensearchableFields(control), thenuserFilters(control):filterableFieldssearchableFieldsuserFiltersexamples/**packages/apps/**packages/qa/**skills/**packages/platform-objects/**The 4
platform-objectshits are the four generated translation bundles, regenerated here.packages/create-objectstackreads 0 for all three keys. Its templates author no list view at all, so that zero is uninformative beyond that fact.Beyond the card's list, these mention the key without authoring it:
packages/lint: the list-view field-ref rule's role and severity maps and its fixtures,REACT_FIELD_SPECS.ListView, the field-consumer vocabulary, and tworule-explanationssentences;packages/metadata-core: the ADR-0106 field-security scrub'sLIST_VIEW_NAME_LISTS;packages/metadata-protocol:computeViewReferenceDiagnostics, a raw-input read;packages/objectql: that read's fixtures.None of those is an author. All are outside this card's file surface and are left in place (Acceptance notes). The tree-scoped pin carries the four files that spell it as an object key as self-expiring RESIDUE.
P3 holds. The mechanism is the one the closest 18.x list-view retirement used (
view-list-tabs-removed), with the strip shape of 17.xview-list-passthrough-keys-removed.retired-keys/18.ui__ListView__filterableFields.tsand one18.ui__ObjectListView__filterableFields.ts, registered per key as gate (b) reads them. One D3semantic/18.list-view-filterable-fields-retired.ts, per family.MAJOR_18_CONVERSIONSwhere its identifier sorts, withorder: 71. It is defined directly aboveviewListTabsRemoved.scripts/conversions-major18-merge.test.tsis green.STEP18_RATIONALEfragment sorts by id, withorder: 105.scripts/step18-rationale-merge.test.tsis green.retiredAfter: '17.7.0'is the newest published stable.retired-after.census.jsonis untouched: it is generated from published tarballs, and its last census release is17.4.0. Under the label18.0.0-next.5,retired-after.census.test.tsadmits[17.4.0, 18.0.0).retired-key-removal-version.pin.test.tsrequires a JOIN row for every retired entry at the newest stable, so one row is added.check-adr-0087-registrationpasses withregistered(both ids new here).no-migration-prescriptionwould refuse itself, because the changeset carries a FROM to TO table.P4 holds. Every generated artifact moved by its own tool:
pnpm --filter @objectstack/spec buildwroteauthorable-surface/ui.json(2 rows to[RETIRED]);gen:docswrote 3 reference pages;gen:liveness-countswrotestate-counts/view.md;node scripts/check-i18n-bundles.mjs --writewrote the 4platform-objectsbundles;gen:migration-registrywrote the gitignored registry.Hand-written, by design: the ledger row (measurement prose, no generator) and the
registry.ts.templatefragment.authorable-surface.base.jsonis untouched. After the merge oforigin/main,check:generatedreads "All 14 generated artifacts are up to date".Mechanism assumptions G1 to G5, measured
G1.
ui/view-list-filterable-fields-retirement.test.tspins the refusal at eight doors:ListViewSchema;ObjectListViewSchema;ViewSchemalist;ViewSchemalistViews.*;ViewItemSchemarecord'sconfig;viewwrite door's flattened overlay;ObjectSchemalistViews.*;list-viewnode'sproperties.Each pin checks issue code
invalid_type, the path naming the key, and the prescription. Each door has two lit controls: the same body without the key parses, and the same body withuserFilters: { element: 'dropdown', fields: [...] }plussearchableFieldsparses. The tsc channel is pinned too (@ts-expect-errorcompiles under the test-layer typecheck), and the parse throws the same prescription.G2. The form row is gone, pinned by a walk of
viewFormwith anti-vacuity neighbours. The bundles are regenerated andcheck:i18npasses.G3.
architecture.mdxis rewritten.check:skill-examplesreports "262 prose examples type-check".G4. As above. The derived changeset gates were run on the final head (Gates).
G5. Every consumer suite is green (Tests). No fixture in those packages authors the key through a parse, so none needed re-truing. The lint and objectql fixtures that spell it feed raw-input walks and still pass.
packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts, where the metadata-form catalog drops 674 to 673. The removed input took its label out of all three locales.Ablation (the tombstone is what shuts every door)
The fix was committed first. Through
scripts/ablation-replace.mjs, in wrap mode under its own restore trap plus an outertrapon the absolute path, the tombstone line was swapped back to the pre-retirement declarationfilterableFields: z.array(z.string()).optional().1e5e34e05ab8tod38c1ae00383, and an in-commandgrep -cread 1 and 0.vitest runof the new pin plusretired-key-removal-version.pin.test.tswent red, the predicted direction. 13 failed and 71 passed.listViewsboundary, whose object now parses;1e5e34e05ab8), andgit diff HEADis empty. The tests importsrcby relative path, so nodistleg applies.Tests (consumer packages
--maxWorkers=2, underos-verify-lock.sh)@objectstack/speclocal: 702 files, 21352 passed (1 todo). Run atc0c1a8ad33and again on the merged headbb71914fc9.@objectstack/specrepoproject: 64 of 71 files, 1039 passed, onbb71914fc9. The new pin is 1 of the 64.build-console-objectstack-root,build-schemas-check-mode,dist-freshness*,publish-smoke-*,published-projection-choke-point). They outlast the foreground cap on this shared box, so they are NOT MEASURED locally and left to CI'sTest Core.@objectstack/spec typecheckpasses, on the merged head too.@objectstack/lint: 148 files, 6935 passed, on the merged head.@objectstack/platform-objects: 79 files, 1195 passed, on the merged head; its typecheck passes.@objectstack/metadata-protocol: 249 files (3 skipped), 20771 passed.@objectstack/objectqllocal: 448 files, 8447 passed.@objectstack/restlocal: 300 files, 6095 passed (326 skipped).@objectstack/runtimelocal: 425 files in three batches, 6671 passed (19 skipped).@objectstack/cliunit: 314 files in four batches, 4641 passed. Theintegrationlayer is declared to CI.c0c1a8ad33, before the merge. The merge brought no newfilterableFieldsmention:git grep -cper file is identical at base and atorigin/main.os validateandos build(app-crm,app-todo,app-showcase,app-multi-package), and all pass.embed-objectqlhas no validate script.Acceptance notes
Out-of-lane readers left in place, for the seat's follow-up. Each reads the key off raw input and none delivers anything once the parse refuses it. This matches the reasoning of the
tabsretirement's stage 2: the lint suite judges the parsed stack.packages/lint/src/validate-list-view-field-refs.ts(role map and severity map) and its test fixtures;packages/lint/src/validate-react-page-props.ts(REACT_FIELD_SPECS.ListView) and its "covers the legacy filterableFields shorthand" test;packages/lint/src/validate-field-consumers.ts:307;packages/lint/src/rule-explanations.ts:3114and:3149, which still name the key as a checked position inos explaintext;packages/lint/src/reference-integrity-suite.ts, in comments;packages/metadata-protocol/src/metadata-diagnostics.ts:237and thepackages/objectql/src/metadata-diagnostics.test.tsfixtures;packages/metadata-core/src/object-schema-fls-references.ts(LIST_VIEW_NAME_LISTS.filterableFields).The tree-scoped pin's RESIDUE set holds the four that spell it as an object key, and goes red when the follow-up deletes them.
Boundary: a react page. A react
ListViewelement'sfilterableFieldsprop lives in a react page's opaquesourcestring, and no door parses that string againstListViewSchema. The lint still treats it as a field-bearing prop ("honored by the renderer"). Once objectui removes its reader, the prop becomes a silent no-op on react pages unless the react-props lint learns to refuse it. The boundary is stated in the schema comment and the ledger note.objectui follow-up (the seat files it). Re-read at the moved pin
12dc7b47:app-shell/src/views/ObjectView.tsx:3448forwards the key;plugin-list/src/ListView.tsx:4656is thefilterFieldswhitelist;plugin-view/src/ObjectView.tsx:3124forwards it,:1030keeps a key list, andplugin-view/src/index.tsx:90registers an input;types/src/objectql.ts:2625and:2966declare it locally;types/src/zod/objectql.zod.ts:1300reads.shape.filterableFieldsby reference.No export is removed and that member still exists as the tombstone, so the pinned objectui build compiles unchanged. The refusal flows into objectui's zod twin on the next pin bump.
ADR-0047 text is now stale. Decision 1 says "
filterableFieldsstays as the legacy shorthand; loaders auto-upgrade it touserFilters.fieldsentries". That auto-upgrade was never built, and the ruling now forbids it. Its own section 4 calls the alias "one-release courtesies, not commitments". An amendment line is adocs/adr/**(Tier H) edit outside this PR's surface. It is raised in the report as an open question for the seat.Out-of-repo consumers: NOT MEASURED.
@objectstack/specis published.Gates
The derivation re-ran on the final head
bb71914fc9with no paths:dispatch-gates --commands --repo objectstack-ai/objectstack, 24 paths against merge base1c1144fcd, giving 115 commands. All 115 were run onbb71914fc9, each exit code captured before any pipe, and--ranreconciles them as "115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED (a DERIVED zero)".check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: 7 unrelated packages had nodist/). It passed after building them.check-adr-0087-registration: "registered view-list-filterable-fields-removed, list-view-filterable-fields-retired (new here: …)";check:api-surface: "public API surface + factory signatures unchanged" (key-level narrowing, no export moved);check:docs: "227 generated files in sync";check:spec-changesandcheck:upgrade-guidegenerate in memory;check:doc-authoring: clean;check:cross-package-test-inputs: OK;check:i18n: OK;check:nul-bytes: OK;check-empty-changesetandcheck-changeset-no-major: pass.check:dev-prereqspasses.check:skill-examples,check:livenessandcheck:generatedare in the 115.Lint & Repo Gates,TypeScript Type Check,Test Core,Build Core) are read from this PR's check-runs. They are not re-run locally.Generated by Claude Code