From 10eb592c24c203215b230173c30dda4666fe1f3f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 13:32:23 +0000 Subject: [PATCH 1/2] fix(showcase): my-work grid authors `filter`, the key object-grid reads (#7750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The personal work queue authored the plural `filters:` on its `object-grid`. objectui's renderer reads only `schema.filter` (`plugin-grid/src/ObjectGrid.tsx`, lowered through `toFilterNode`) and the legacy `schema.defaultFilters`; `schema.filters` has zero read points on any ref. The declared personal scope was therefore accepted at authoring time and dropped before the wire — no `$filter` parameter at all, and the queue listed every row. objectui#4041 (`9154d9e`) retired the plural from the block's published vocabulary, so `filter` is now the declared, pinned and lowered spelling. That commit is an ancestor of this repo's `.objectui-sha` pin `6314e87f2` (merge_base == `9154d9e`, behind_by 0). Not an authorization bypass: the unfiltered read is still RLS-constrained, so the caller only ever saw rows they may see. The defect is that a declared personal-scope filter silently never applied. Pins added to the existing `my-work-visibility.test.ts` (no new test file, so no TEST_DEBT drift): the grid authors `filter`, carries neither dropped spelling, and still scopes to `{current_user_id}`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BaTeBCjaoTDmDWFVmSY2jL --- .../app-showcase/src/ui/pages/my-work.page.ts | 9 ++- .../test/my-work-visibility.test.ts | 60 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/examples/app-showcase/src/ui/pages/my-work.page.ts b/examples/app-showcase/src/ui/pages/my-work.page.ts index ce5453a7a8..e6e1143b37 100644 --- a/examples/app-showcase/src/ui/pages/my-work.page.ts +++ b/examples/app-showcase/src/ui/pages/my-work.page.ts @@ -44,12 +44,19 @@ export const MyWorkPage = definePage({ }, { type: 'element:divider', properties: {} }, // Personal work queue — records owned by the signed-in user. + // `filter`, SINGULAR: that is the key `object-grid` both publishes and + // reads (objectui `plugin-grid/src/index.tsx` declares + // `{ name: 'filter', … }`; `ObjectGrid.tsx` reads `schema.filter` and + // lowers it through `toFilterNode` to `$filter`). The plural spelling + // this line used to carry has ZERO read points in the renderer, so it + // was accepted and then dropped — the queue silently listed every row + // (objectstack#7750). { type: 'object-grid', properties: { objectName: 'showcase_task', columns: ['title', 'project', 'status', 'priority', 'due_date'], - filters: [['owner_id', '=', '{current_user_id}']], + filter: [['owner_id', '=', '{current_user_id}']], }, }, ], diff --git a/examples/app-showcase/test/my-work-visibility.test.ts b/examples/app-showcase/test/my-work-visibility.test.ts index 5f7e268bfb..2be2ee5f7a 100644 --- a/examples/app-showcase/test/my-work-visibility.test.ts +++ b/examples/app-showcase/test/my-work-visibility.test.ts @@ -67,6 +67,36 @@ const leadershipCard = () => (c) => c.type === 'page:card' && c.properties?.title === 'Leadership View', ); +const workQueueGrid = () => allComponents().find((c) => c.type === 'object-grid'); + +/** + * The personal work queue's filter key, in the ONE spelling `object-grid` + * publishes and reads (objectstack#7750). + * + * This page authored the plural `filters:`. objectui's renderer reads only + * `schema.filter` (`plugin-grid/src/ObjectGrid.tsx`, lowered through + * `toFilterNode`) and the legacy `schema.defaultFilters`; `schema.filters` has + * zero read points on any ref. So the declared personal scope was accepted at + * authoring time and then dropped before the wire — no `$filter` parameter at + * all, and the "my work" queue listed every row. + * + * ⚠️ Not an authorization bypass: the unfiltered read is still RLS-constrained, + * so the caller only ever saw rows they may see. The defect is that a DECLARED + * personal-scope filter silently never applied. + * + * What these assertions can and cannot prove, stated honestly: + * - they pin the AUTHORED key against the spelling objectui declares, which is + * the whole of what is checkable inside this repo; + * - they do NOT prove `$filter` reaches the wire. That is objectui's own pin + * (`gridFilterInputSpelling.test.tsx`, objectui#4041 / `9154d9e`), and + * driving it end-to-end here needs the vendored `packages/console/dist` + * rebuilt at the current pin — tracked as objectstack#7752. + * + * The sibling `object-metric` tiles on this same page already spell `filter` + * singular, which is why the grid was the only site that drifted. + */ +const PLURAL_FILTER_SPELLINGS = ['filters', 'defaultFilters'] as const; + describe('My Work — admin-only card gating (ADR-0089 component-level visibleWhen)', () => { it('carries the predicate at the COMPONENT level, not inside `properties`', () => { const card = leadershipCard(); @@ -106,3 +136,33 @@ describe('My Work — admin-only card gating (ADR-0089 component-level visibleWh expect(evalPredicate(source, { current_user: { email: 'analyst@objectos.ai' } })).toBe(false); }); }); + +describe('My Work — the personal queue declares its filter in the key object-grid reads', () => { + it('authors `filter` (singular) on the work-queue grid', () => { + const grid = workQueueGrid(); + expect(grid, 'the personal work queue `object-grid` must exist').toBeTruthy(); + expect( + grid!.properties, + '`object-grid` publishes and reads `filter` — the singular key it lowers to `$filter`', + ).toHaveProperty('filter'); + }); + + it('carries no spelling the renderer drops', () => { + // Pins the regression directly: a plural key here parses, renders, and + // silently widens the query to the object's full scope. + for (const key of PLURAL_FILTER_SPELLINGS) { + expect( + workQueueGrid()!.properties, + `\`${key}\` is not the key object-grid reads — it is dropped before the wire (#7750)`, + ).not.toHaveProperty(key); + } + }); + + it('still scopes the queue to the signed-in user', () => { + // The spelling is only half the fix; the predicate is what makes the page + // personal at all. `{current_user_id}` is the page-level identity token. + expect(workQueueGrid()!.properties!.filter).toEqual([ + ['owner_id', '=', '{current_user_id}'], + ]); + }); +}); From 61022c24fc73ae3fdbf5489ec45c58bdf3df4dbc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 14:31:10 +0000 Subject: [PATCH 2/2] test(showcase): give each non-canonical filter spelling its own true reason (#7750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared assertion message called both `filters` and `defaultFilters` "dropped before the wire". That is true of `filters` and FALSE of `defaultFilters`: `ObjectGrid.tsx` reads it and lowers it to `params.$filter`, so it is the legacy path object-grid still honors, not a dead key. Left as written, a future author debugging a filter bug would read the message and conclude the legacy key is inert — the same defect this card fixes, a spelling whose real behavior differs from what the surrounding text implies, reproduced in the artifact whose job is to prevent it. Both keys stay pinned absent; only the justification changes. `filters` because nothing reads it, `defaultFilters` because this page authors the current key rather than the superseded one. Split the shared string into a per-key table and reworded the `it` so "dropped" attaches only to `filters`. Text only — no assertion's truth value changed, and the page fix is untouched. Both messages verified by authoring each key in turn and reading the failure. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BaTeBCjaoTDmDWFVmSY2jL --- .../test/my-work-visibility.test.ts | 87 ++++++++++++------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/examples/app-showcase/test/my-work-visibility.test.ts b/examples/app-showcase/test/my-work-visibility.test.ts index 2be2ee5f7a..558f2f80a6 100644 --- a/examples/app-showcase/test/my-work-visibility.test.ts +++ b/examples/app-showcase/test/my-work-visibility.test.ts @@ -70,32 +70,33 @@ const leadershipCard = () => const workQueueGrid = () => allComponents().find((c) => c.type === 'object-grid'); /** - * The personal work queue's filter key, in the ONE spelling `object-grid` - * publishes and reads (objectstack#7750). - * - * This page authored the plural `filters:`. objectui's renderer reads only - * `schema.filter` (`plugin-grid/src/ObjectGrid.tsx`, lowered through - * `toFilterNode`) and the legacy `schema.defaultFilters`; `schema.filters` has - * zero read points on any ref. So the declared personal scope was accepted at - * authoring time and then dropped before the wire — no `$filter` parameter at - * all, and the "my work" queue listed every row. - * - * ⚠️ Not an authorization bypass: the unfiltered read is still RLS-constrained, - * so the caller only ever saw rows they may see. The defect is that a DECLARED - * personal-scope filter silently never applied. + * The two non-canonical spellings this grid must not carry — and the DIFFERENT + * reason each one is absent. They share an assertion; they must never share a + * justification, because only one of them is dead: * - * What these assertions can and cannot prove, stated honestly: - * - they pin the AUTHORED key against the spelling objectui declares, which is - * the whole of what is checkable inside this repo; - * - they do NOT prove `$filter` reaches the wire. That is objectui's own pin - * (`gridFilterInputSpelling.test.tsx`, objectui#4041 / `9154d9e`), and - * driving it end-to-end here needs the vendored `packages/console/dist` - * rebuilt at the current pin — tracked as objectstack#7752. + * - **`filters` is DEAD.** Zero read points in the renderer on any ref, so it + * is accepted at authoring time and dropped before the wire. That is the + * #7750 defect itself. + * - **`defaultFilters` is ALIVE.** `ObjectGrid.tsx` reads it and lowers it to + * `params.$filter` — the legacy path `object-grid` still honors. It is + * pinned absent because this page authors the CURRENT key, NOT because the + * legacy one is inert. * - * The sibling `object-metric` tiles on this same page already spell `filter` - * singular, which is why the grid was the only site that drifted. + * That distinction is the whole point of the card, so getting it wrong here + * would reproduce the defect one level up: a future author debugging a filter + * bug must not read this test as licence to treat `defaultFilters` as a no-op, + * nor reach for it as a workaround. */ -const PLURAL_FILTER_SPELLINGS = ['filters', 'defaultFilters'] as const; +const NON_CANONICAL_FILTER_SPELLINGS: ReadonlyArray<{ key: string; why: string }> = [ + { + key: 'filters', + why: 'has no reader in `object-grid` — it is accepted at authoring time and then dropped before the wire (#7750)', + }, + { + key: 'defaultFilters', + why: 'is the LEGACY key `object-grid` still reads and lowers to `$filter` — it works, but it is not the key this page declares', + }, +]; describe('My Work — admin-only card gating (ADR-0089 component-level visibleWhen)', () => { it('carries the predicate at the COMPONENT level, not inside `properties`', () => { @@ -137,6 +138,32 @@ describe('My Work — admin-only card gating (ADR-0089 component-level visibleWh }); }); +/** + * The personal work queue's filter key, in the ONE spelling `object-grid` + * publishes and reads (objectstack#7750). + * + * This page authored the plural `filters:`. objectui's renderer reads only + * `schema.filter` (`plugin-grid/src/ObjectGrid.tsx`, lowered through + * `toFilterNode`) and the legacy `schema.defaultFilters`; `schema.filters` has + * zero read points on any ref. So the declared personal scope was accepted at + * authoring time and then dropped before the wire — no `$filter` parameter at + * all, and the "my work" queue listed every row. + * + * ⚠️ Not an authorization bypass: the unfiltered read is still RLS-constrained, + * so the caller only ever saw rows they may see. The defect is that a DECLARED + * personal-scope filter silently never applied. + * + * What these assertions can and cannot prove, stated honestly: + * - they pin the AUTHORED key against the spelling objectui declares, which is + * the whole of what is checkable inside this repo; + * - they do NOT prove `$filter` reaches the wire. That is objectui's own pin + * (`gridFilterInputSpelling.test.tsx`, objectui#4041 / `9154d9e`), and + * driving it end-to-end here needs the vendored `packages/console/dist` + * rebuilt at the current pin — tracked as objectstack#7752. + * + * The sibling `object-metric` tiles on this same page already spell `filter` + * singular, which is why the grid was the only site that drifted. + */ describe('My Work — the personal queue declares its filter in the key object-grid reads', () => { it('authors `filter` (singular) on the work-queue grid', () => { const grid = workQueueGrid(); @@ -147,14 +174,12 @@ describe('My Work — the personal queue declares its filter in the key object-g ).toHaveProperty('filter'); }); - it('carries no spelling the renderer drops', () => { - // Pins the regression directly: a plural key here parses, renders, and - // silently widens the query to the object's full scope. - for (const key of PLURAL_FILTER_SPELLINGS) { - expect( - workQueueGrid()!.properties, - `\`${key}\` is not the key object-grid reads — it is dropped before the wire (#7750)`, - ).not.toHaveProperty(key); + it('authors neither non-canonical spelling: `filters`, which is dropped, nor the legacy `defaultFilters`', () => { + // Each key fails for its OWN reason, so each carries its own message — + // `filters` because nothing reads it, `defaultFilters` because it is the + // superseded spelling rather than a dead one. + for (const { key, why } of NON_CANONICAL_FILTER_SPELLINGS) { + expect(workQueueGrid()!.properties, `\`${key}\` ${why}`).not.toHaveProperty(key); } });