Repository navigation
Commit 4bc3bec
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)
Fixes #24640
Clause-②: yes (narrowing)
## What
`filterableFields` retires 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
for `userFilters.fields` (the quick-filter bar), and the Studio view
form offered it as "Field names available for filtering". Its one
renderer, objectui's `ListView` (the `filterFields` memo), 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**.
- **Tombstone.** `filterableFields:
retiredKey(LIST_VIEW_FILTERABLE_FIELDS_RETIRED)` sits in
`ListViewShapeSchema`, beside `searchableFields`. Every door built from
the shape refuses the key with the prescription: `tsc` types it `never`,
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 declare
`userFilters: { 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 marks `hidden: true` is not
offered). It closes with the house `os migrate meta --from 17` sentence.
- **Studio form.** The `filterableFields` input leaves `view.form.ts`,
with a comment in its place. The four extracted form-label bundles are
regenerated (pure deletion).
- **ADR-0087.** The D2 conversion `view-list-filterable-fields-removed`
(protocol 18, `retiredFromLoadPath: true`, `retiredAfter: '17.7.0'`)
**drops** the key from every list payload in `stack.views[]`, in all
three persisted spellings. It never rewrites it to `userFilters.fields`.
The D3 entry is `list-view-filterable-fields-retired`.
`RETIRED_KEYS_BY_MAJOR[18]` gains `ui/ListView:filterableFields` and
`ui/ObjectListView:filterableFields`. The step-18 rationale gains one
fragment.
- **Ledger.** The `view.json` row
`/props/list/children/filterableFields` flips `live` to `dead`. It keeps
the tombstone discipline (`verifiedAt` 2026-10-19, `evidenceScope:
cross-repo`) and carries a REMOVED note naming the one reader the ruling
makes unreachable. `state-counts/view.md` is regenerated: live 79 to 78,
dead 11 to 12.
- **Docs.** The `os:check` example on
`content/docs/concepts/architecture.mdx` stops teaching the key. It
declares a `userFilters` dropdown on `industry` instead, because the
example meant a user-facing filter. The reference pages are regenerated.
- **Changeset.**
`.changeset/24640-list-view-filterable-fields-retired.md`:
`@objectstack/spec` minor, 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 marker `registered
view-list-filterable-fields-removed,
list-view-filterable-fields-retired`. Release arithmetic, measured:
`.changeset/pre.json` is `{ "mode": "exit", "tag": "next" }` and the
spec label is `18.0.0-next.5`. A minor increment of that prerelease is
`18.0.0`, so the next version pass publishes this in 18.0.0.
## Premises, each with its reading (base `bfc1539fe0`; re-read after
merging `origin/main` `1c1144fcd8`)
**P1 holds.** One declaration exists: `packages/spec/src/ui/view.zod.ts`
(was `:2844`) on `ListViewShapeSchema`. `ObjectListViewSchema` is
`ListViewShapeSchema.omit({ userFilters }).extend(...)`, so one
tombstone covers both. Both `authorable-surface/ui.json` rows (`:757`,
`:944`) now read `[RETIRED]`. Inside `packages/spec/src`, `git grep -c
filterableFields` at 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 page `list-view` node,
`ComponentPropsMap['list-view']`) spreads `ListViewSchema.shape`. The
react `ListView` block's schema is `ListViewSchema`, and its curated
`dataProps` never 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 -c` for `filterableFields`, then `searchableFields` (control),
then `userFilters` (control):
| corpus | `filterableFields` | `searchableFields` | `userFilters` |
| --- | --- | --- | --- |
| `examples/**` | 0 | 2 | 7 |
| `packages/apps/**` | 0 | 0 | 0 |
| `packages/qa/**` | 0 | 2 | 0 |
| `skills/**` | 0 | 20 | 12 |
| `packages/platform-objects/**` | 4 | 12 | 21 |
The 4 `platform-objects` hits are the four generated translation
bundles, regenerated here.
- `packages/create-objectstack` reads 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 two `rule-explanations` sentences;
- `packages/metadata-core`: the ADR-0106 field-security scrub's
`LIST_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.x
`view-list-passthrough-keys-removed`.
- **Entries.** One `retired-keys/18.ui__ListView__filterableFields.ts`
and one `18.ui__ObjectListView__filterableFields.ts`, registered per key
as gate (b) reads them. One D3
`semantic/18.list-view-filterable-fields-retired.ts`, per family.
- **Conversion order.** The conversion is inserted into
`MAJOR_18_CONVERSIONS` where its identifier sorts, with `order: 71`. It
is defined directly above `viewListTabsRemoved`.
`scripts/conversions-major18-merge.test.ts` is green.
- **Rationale.** The `STEP18_RATIONALE` fragment sorts by id, with
`order: 105`. `scripts/step18-rationale-merge.test.ts` is green.
- **`retiredAfter: '17.7.0'`** is the newest published stable.
`retired-after.census.json` is untouched: it is generated from published
tarballs, and its last census release is `17.4.0`. Under the label
`18.0.0-next.5`, `retired-after.census.test.ts` admits `[17.4.0,
18.0.0)`.
- **The pin's JOIN row.** `retired-key-removal-version.pin.test.ts`
requires a JOIN row for every retired entry at the newest stable, so one
row is added.
- **Disposition.** `check-adr-0087-registration` passes with
`registered` (both ids new here). `no-migration-prescription` would
refuse itself, because the changeset carries a FROM to TO table.
- **Not a semantic mapping.** The machinery asked for a conversion. It
is a drop, so this premise did not fail.
**P4 holds.** Every generated artifact moved by its own tool:
- `pnpm --filter @objectstack/spec build` wrote
`authorable-surface/ui.json` (2 rows to `[RETIRED]`);
- `gen:docs` wrote 3 reference pages;
- `gen:liveness-counts` wrote `state-counts/view.md`;
- `node scripts/check-i18n-bundles.mjs --write` wrote the 4
`platform-objects` bundles;
- `gen:migration-registry` wrote the gitignored registry.
Hand-written, by design: the ledger row (measurement prose, no
generator) and the `registry.ts.template` fragment.
`authorable-surface.base.json` is untouched. After the merge of
`origin/main`, `check:generated` reads "All 14 generated artifacts are
up to date".
## Mechanism assumptions G1 to G5, measured
- **G1.** `ui/view-list-filterable-fields-retirement.test.ts` pins the
refusal at **eight** doors:
- `ListViewSchema`;
- `ObjectListViewSchema`;
- `ViewSchema` `list`;
- `ViewSchema` `listViews.*`;
- a `ViewItemSchema` record's `config`;
- the `view` write door's flattened overlay;
- `ObjectSchema` `listViews.*`;
- a page `list-view` node's `properties`.
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 with `userFilters: { element:
'dropdown', fields: [...] }` plus `searchableFields` parses. The tsc
channel is pinned too (`@ts-expect-error` compiles under the test-layer
typecheck), and the parse throws the same prescription.
- **G2.** The form row is gone, pinned by a walk of `viewForm` with
anti-vacuity neighbours. The bundles are regenerated and `check:i18n`
passes.
- **G3.** `architecture.mdx` is rewritten. `check:skill-examples`
reports "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.
- One out-of-lane pin had to move. It is a count, not a fixture:
`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.
- That edit is a cross-lane rider, listed for the seat to declare.
## 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 outer `trap` on the
absolute path, the tombstone line was swapped back to the pre-retirement
declaration `filterableFields: z.array(z.string()).optional()`.
- **Mutation on disk:** anchor count 1 to 0 and replacement count 0 to
1. The blob moved from `1e5e34e05ab8` to `d38c1ae00383`, and an
in-command `grep -c` read 1 and 0.
- **Run:** `vitest run` of the new pin plus
`retired-key-removal-version.pin.test.ts` went **red, the predicted
direction**. 13 failed and 71 passed.
- **What failed:**
- all 8 door refusals;
- the empty-array refusal;
- the prescription content;
- the tsc-channel parse;
- the object-`listViews` boundary, whose object now parses;
- the removal-version JOIN row.
- **What held:** every control and boundary acceptance, and every D2
conversion pin. Those pins are independent of the shape.
- **Restore:** the blob after restore equals HEAD (`1e5e34e05ab8`), and
`git diff HEAD` is empty. The tests import `src` by relative path, so no
`dist` leg applies.
## Tests (consumer packages `--maxWorkers=2`, under `os-verify-lock.sh`)
- `@objectstack/spec` `local`: 702 files, 21352 passed (1 todo). Run at
`c0c1a8ad33` and again on the merged head `bb71914fc9`.
- `@objectstack/spec` `repo` project: 64 of 71 files, 1039 passed, on
`bb71914fc9`. The new pin is 1 of the 64.
- The other 7 are the build and publish tooling tests
(`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's `Test
Core`.
- `@objectstack/spec typecheck` passes, 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/objectql` `local`: 448 files, 8447 passed.
- `@objectstack/rest` `local`: 300 files, 6095 passed (326 skipped).
- `@objectstack/runtime` `local`: 425 files in three batches, 6671
passed (19 skipped).
- `@objectstack/cli` `unit`: 314 files in four batches, 4641 passed. The
`integration` layer is declared to CI.
- These five were run at `c0c1a8ad33`, before the merge. The merge
brought no new `filterableFields` mention: `git grep -c` per file is
identical at base and at `origin/main`.
- Every example app runs `os validate` and `os build` (`app-crm`,
`app-todo`, `app-showcase`, `app-multi-package`), and all pass.
`embed-objectql` has 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 `tabs` retirement'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:3114` and `:3149`, which still
name the key as a checked position in `os explain` text;
- `packages/lint/src/reference-integrity-suite.ts`, in comments;
- `packages/metadata-protocol/src/metadata-diagnostics.ts:237` and the
`packages/objectql/src/metadata-diagnostics.test.ts` fixtures;
- `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 `ListView` element's
`filterableFields` prop lives in a react page's opaque `source` string,
and no door parses that string against `ListViewSchema`. 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:3448` forwards the key;
- `plugin-list/src/ListView.tsx:4656` is the `filterFields` whitelist;
- `plugin-view/src/ObjectView.tsx:3124` forwards it, `:1030` keeps a key
list, and `plugin-view/src/index.tsx:90` registers an input;
- `types/src/objectql.ts:2625` and `:2966` declare it locally;
- `types/src/zod/objectql.zod.ts:1300` reads `.shape.filterableFields`
by 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 "`filterableFields`
stays as the legacy shorthand; loaders auto-upgrade it to
`userFilters.fields` entries". 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 a
`docs/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/spec` is
published.
## Gates
The derivation re-ran on the final head `bb71914fc9` with no paths:
`dispatch-gates --commands --repo objectstack-ai/objectstack`, 24 paths
against merge base `1c1144fcd`, giving 115 commands. All 115 were run on
`bb71914fc9`, each exit code captured before any pipe, and `--ran`
reconciles them as "115 derived famil(ies) accounted for — 115 run, 0
NOT-MEASURED (a DERIVED zero)".
- One gate needed its prerequisite first. `check:dual-build-cjs-loads`
first exited 3 (PREREQUISITE NOT MET: 7 unrelated packages had no
`dist/`). It passed after building them.
- Key verdict lines:
- `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-changes` and `check:upgrade-guide` generate in memory;
- `check:doc-authoring`: clean;
- `check:cross-package-test-inputs`: OK;
- `check:i18n`: OK;
- `check:nul-bytes`: OK;
- `check-empty-changeset` and `check-changeset-no-major`: pass.
- Also run locally: the local-only `check:dev-prereqs` passes.
`check:skill-examples`, `check:liveness` and `check:generated` are in
the 115.
- CI's own runs (`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](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0904d85 commit 4bc3bec
24 files changed
Lines changed: 944 additions & 37 deletions
File tree
- .changeset
- content/docs
- concepts
- references
- api
- data
- ui
- packages
- platform-objects/src/apps/translations
- spec
- authorable-surface
- liveness
- state-counts
- src
- conversions
- migrations
- entries
- retired-keys
- semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
245 | | - | |
| 245 | + | |
| 246 | + | |
246 | 247 | | |
247 | 248 | | |
248 | 249 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1831 | 1831 | | |
1832 | 1832 | | |
1833 | 1833 | | |
1834 | | - | |
| 1834 | + | |
1835 | 1835 | | |
1836 | 1836 | | |
1837 | 1837 | | |
| |||
1917 | 1917 | | |
1918 | 1918 | | |
1919 | 1919 | | |
1920 | | - | |
| 1920 | + | |
1921 | 1921 | | |
1922 | 1922 | | |
1923 | 1923 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
377 | 377 | | |
378 | 378 | | |
379 | 379 | | |
380 | | - | |
| 380 | + | |
381 | 381 | | |
382 | 382 | | |
383 | 383 | | |
| |||
0 commit comments