From 4f48e528447bc381735646b3077ec906d90bca65 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 17:18:54 +0000 Subject: [PATCH 1/3] fix(types): the stray-groupBy kanban refusal no longer denies the validation that emits it (objectui#9653) `KanbanStrayGroupByRefusal`'s message said a view carrying `kanban.groupBy` "never came through the validated path", then that the key rode this mirror's `.passthrough()` into `ListView` and changed the board. Only the second was true: before the refusal, `safeValidateSchema` accepted and kept the key in both nestings and `ListView` grouped by it. The false clause is gone; the tail now says what happened and why the key is refused now. What is refused (key, paths, codes, lead sentence, remedy) is unchanged. Also corrects `checkNamedViewCalendarAliases`' docblock, which said every alias refusal in the module "stops at `listViews`" while the function itself refuses `listViews.KEY.calendar.dateField`. Adds a content pin (no clause denying validation; the message names the validator and the mechanism that kept the key) and a patch changeset. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC --- .../9653-kanban-stray-groupby-refusal-text.md | 25 ++++++ ...n-stray-group-by-refusal-text-9653.test.ts | 84 +++++++++++++++++++ packages/types/src/zod/objectql.zod.ts | 28 ++++--- 3 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 .changeset/9653-kanban-stray-groupby-refusal-text.md create mode 100644 packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts diff --git a/.changeset/9653-kanban-stray-groupby-refusal-text.md b/.changeset/9653-kanban-stray-groupby-refusal-text.md new file mode 100644 index 0000000000..3b27654ac5 --- /dev/null +++ b/.changeset/9653-kanban-stray-groupby-refusal-text.md @@ -0,0 +1,25 @@ +--- +"@object-ui/types": patch +--- + +**The stray-`groupBy` kanban refusal no longer tells an author their view "never came through the validated path".** + +The refusal an author meets for a view-level `kanban.groupBy` (objectui#8365) said +that a view carrying the key "never came through the validated path", and, in the +same message, that the key "rode this object's `.passthrough()`" into `ListView` +and changed the board. Only the second was true. Measured on the tree before the +refusal landed: `safeValidateSchema` accepted the key green in both nestings and +kept it, and `ListView` grouped the board by it. The protocol is what refuses the +key (`@objectstack/spec`'s strict `KanbanConfigSchema`); this package did not. + +The false clause is gone. The message's explanatory tail now says what happened: +this package accepted the key until the refusal, kept by the kanban +configuration's `.passthrough()` or by the untyped legacy `options` bag, and +`ListView` let it override the lane resolved from `groupByField`. It also says +why the key is refused now: `ListView` drops it, so it is refused instead of +being kept and then ignored. + +What is refused is unchanged: the same key at the same two paths +(`kanban.groupBy` as `invalid_type`, `options.kanban.groupBy` as `custom`), with +the same lead sentence and the same remedy. Only the explanatory tail of the +message changed. diff --git a/packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts b/packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts new file mode 100644 index 0000000000..6c283dac96 --- /dev/null +++ b/packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts @@ -0,0 +1,84 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#9653 — `KanbanStrayGroupByRefusal`'s message must not deny the + * validation that emits it. + * + * The objectui#8365 message said a view carrying `groupBy` "never came through + * the validated path", and a few clauses later that the key rode this mirror's + * `.passthrough()` into `ListView` and changed the board. Only the second was + * true: at the parent of the commit that added the refusal, `safeValidateSchema` + * ACCEPTED the key in both nestings and KEPT it, and `ListView` grouped the board + * by it. The protocol is what refuses it (`@objectstack/spec`'s strict + * `KanbanConfigSchema`); this package did not, until the refusal. + * + * The message is read by an AUTHOR, from this package's own validator — so a + * clause telling them their document was never validated is false at the very + * place they read it. The same false clause was caught once before, in + * objectui#8355's calendar stem, before it landed (objectui#9648). + * + * ## What is asserted, and why not the wording + * + * The refusal itself (`code` and `path` on both channels) is pinned by the + * objectui#8365 tests; this file re-reads it only to find the message. It does + * NOT pin the message's sentences. It asserts two things about CONTENT: + * + * - NEGATIVE — no clause denies that the document passed a validator. + * - LIT CONTROL, on the true clause — the message still names the validator + * that accepted the key (`safeValidateSchema`) and the mechanism that kept it + * (`.passthrough()`). Without it, the negative is satisfied by a message + * that says nothing about the history at all. + * + * A third arm proves the negative's matcher fires on the retired clause, so a + * later loosening of the pattern cannot turn the negative into a no-op. + * + * REVERSE VERIFICATION, predicted before running: restore the retired clause + * in the message ⇒ the NEGATIVE arm goes red on both channels, while the lit + * control and the matcher control stay green. + */ + +import { describe, it, expect } from 'vitest'; +import { ListViewSchema } from '../zod/objectql.zod'; + +/** A clause that tells the author their document never passed a validator. */ +const DENIES_VALIDATION = + /never\s+(?:came|went|passed)\s+through\s+(?:the\s+|a\s+)?(?:validated\s+path|validator|validation)/i; + +/** The refusal message at one channel, read through the published door. */ +function refusalMessage(doc: Record, path: string, code: string): string { + const result = ListViewSchema.safeParse({ type: 'list-view', objectName: 'deal', ...doc }); + expect(result.success, 'the fixture must be refused, or there is no message to read').toBe(false); + const issue = result.success ? undefined : result.error.issues.find((i) => i.path.join('.') === path); + expect(issue?.code).toBe(code); + return issue?.message ?? ''; +} + +const CHANNELS = [ + { name: 'declared `kanban` slot', doc: { kanban: { groupByField: 'stage', groupBy: 'stage' } }, path: 'kanban.groupBy', code: 'invalid_type' }, + { name: 'legacy `options.kanban` bag', doc: { options: { kanban: { groupBy: 'stage' } } }, path: 'options.kanban.groupBy', code: 'custom' }, +] as const; + +describe('objectui#9653 · the stray-`groupBy` refusal does not deny the validation that emits it', () => { + for (const channel of CHANNELS) { + it(`NEGATIVE (${channel.name}): no clause says the document never passed a validator`, () => { + const message = refusalMessage(channel.doc, channel.path, channel.code); + expect(message).not.toMatch(DENIES_VALIDATION); + }); + + it(`LIT CONTROL (${channel.name}): the message names the validator that accepted the key and the mechanism that kept it`, () => { + const message = refusalMessage(channel.doc, channel.path, channel.code); + expect(message).toContain('`safeValidateSchema`'); + expect(message).toContain('`.passthrough()`'); + }); + } + + it('MATCHER CONTROL: the negative fires on the retired clause', () => { + expect('so a view carrying it never came through the validated path.').toMatch(DENIES_VALIDATION); + }); +}); diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 32b74013dd..00d6d77f80 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -841,11 +841,13 @@ const KanbanStrayGroupByRefusal = aliasKeyRefusal( '`groupBy` is the lane key of the generated `object-kanban` NODE, not of the view-level ' + 'kanban configuration (objectui#8365). `@objectstack/spec`\'s `KanbanConfigSchema` is a ' + 'strict object of `columns` / `groupByField` / `summarizeField` and refuses `groupBy` by ' - + 'name, so a view carrying it never came through the validated path. Write `groupByField` ' - + '(or the deprecated `groupField`, which folds onto it). Until this refusal the key rode ' - + 'this object\'s `.passthrough()` into `ListView`\'s kanban branch and OVERRODE the lane ' - + 'that branch had already resolved from `groupByField` — the board grouped by the stray ' - + 'key, and nothing said so.', + + 'name. Write `groupByField` (or the deprecated `groupField`, which folds onto it). This ' + + 'package, by contrast, accepted the key green until this refusal: it passed ' + + '`safeValidateSchema` in either nesting, kept by this object\'s `.passthrough()` or by the ' + + 'untyped legacy `options` bag, and reached `ListView`\'s kanban branch, where it OVERRODE the ' + + 'lane that branch had already resolved from `groupByField` — the board grouped by the stray ' + + 'key, and nothing said so. That branch now drops the key, so it is refused here instead of ' + + 'being kept and then ignored.', ); /** @@ -1104,12 +1106,16 @@ const CalendarNodeDateAliasRefusals = { * check is for. The gap here is NOT specific to the calendar and was NOT opened * by this card: measured on the same instrument, a named view carrying the * objectui#8365 stray `kanban.groupBy` is ACCEPTED, while the identical key on a - * `list-view` document is refused. Every alias refusal this module declares - * stops at `listViews`. What this card regressed on the object-view route is the - * BEHAVIOUR — a document that drew at the merge-base goes mute once the ladder - * is gone — and this check is what makes that failure loud. ⛔ It is not a - * general repair of the unmirrored key, and the kanban twin is still silent - * here; that belongs to objectui#8365's own text, not to this card. + * `list-view` document is refused. Until this check, no alias refusal this + * module declares reached inside `listViews`. This check is the only door in, + * and it judges the two calendar spellings only, at both nestings (so + * `listViews.KEY.calendar.dateField` IS refused); a search for `listViews` in + * this file re-derives that. What this card regressed on the object-view route + * is the BEHAVIOUR — a document that drew at the merge-base goes mute once the + * ladder is gone — and this check is what makes that failure loud. ⛔ It is not + * a general repair of the unmirrored key, and the kanban twin is still silent + * here (no pin re-derives that silence); that belongs to objectui#8365's own + * text, not to this card. * * ⛔ Scoped to the TWO keys under `calendar`: `timeline.dateField` on a named * view stays accepted (the timeline alias is live by ruling), and nothing else From e9fce805977ef9cde3f76c01e5e6fa0910fe0713 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 17:22:06 +0000 Subject: [PATCH 2/3] test(types): the 9653 lit control reads the passthrough clause only (objectui#9653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lit control required the message to name `safeValidateSchema` as well, which pinned new wording rather than the true clause, and would have gone red together with the negative when the retired text is restored — so the ablation could not tell the two arms apart. It now asserts the passthrough clause alone, which the retired text also carried. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC --- ...kanban-stray-group-by-refusal-text-9653.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts b/packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts index 6c283dac96..966dd7a6f2 100644 --- a/packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts +++ b/packages/types/src/__tests__/kanban-stray-group-by-refusal-text-9653.test.ts @@ -30,17 +30,18 @@ * NOT pin the message's sentences. It asserts two things about CONTENT: * * - NEGATIVE — no clause denies that the document passed a validator. - * - LIT CONTROL, on the true clause — the message still names the validator - * that accepted the key (`safeValidateSchema`) and the mechanism that kept it - * (`.passthrough()`). Without it, the negative is satisfied by a message - * that says nothing about the history at all. + * - LIT CONTROL, on the true clause — the message still names the mechanism + * that kept the key (`.passthrough()`). Without it, the negative is satisfied + * by a message that says nothing about the history at all. * * A third arm proves the negative's matcher fires on the retired clause, so a * later loosening of the pattern cannot turn the negative into a no-op. * * REVERSE VERIFICATION, predicted before running: restore the retired clause * in the message ⇒ the NEGATIVE arm goes red on both channels, while the lit - * control and the matcher control stay green. + * control and the matcher control stay green (the retired text described the + * passthrough too — that is the contradiction). Drop the history clause + * instead ⇒ the lit control goes red and the negative stays green. */ import { describe, it, expect } from 'vitest'; @@ -71,9 +72,8 @@ describe('objectui#9653 · the stray-`groupBy` refusal does not deny the validat expect(message).not.toMatch(DENIES_VALIDATION); }); - it(`LIT CONTROL (${channel.name}): the message names the validator that accepted the key and the mechanism that kept it`, () => { + it(`LIT CONTROL (${channel.name}): the message still describes the passthrough that kept the key`, () => { const message = refusalMessage(channel.doc, channel.path, channel.code); - expect(message).toContain('`safeValidateSchema`'); expect(message).toContain('`.passthrough()`'); }); } From ee1f2a7aa8fee31d7681cc91dadc31c61355030a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 17:26:21 +0000 Subject: [PATCH 3/3] docs(changeset): name the shipped docblock correction in the 9653 changeset (objectui#9653) A build of `@object-ui/types` shows the named-view calendar check's docblock is emitted into the built JavaScript as a comment, so the correction ships too; the changeset now says so. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC --- .changeset/9653-kanban-stray-groupby-refusal-text.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.changeset/9653-kanban-stray-groupby-refusal-text.md b/.changeset/9653-kanban-stray-groupby-refusal-text.md index 3b27654ac5..bbcb6b44b8 100644 --- a/.changeset/9653-kanban-stray-groupby-refusal-text.md +++ b/.changeset/9653-kanban-stray-groupby-refusal-text.md @@ -23,3 +23,10 @@ What is refused is unchanged: the same key at the same two paths (`kanban.groupBy` as `invalid_type`, `options.kanban.groupBy` as `custom`), with the same lead sentence and the same remedy. Only the explanatory tail of the message changed. + +Also corrected, and shipped only as a comment in the built JavaScript: the +docblock of the named-view calendar check on `ObjectViewSchema` said every alias +refusal in the module "stops at `listViews`", while that check itself refuses +`listViews.KEY.calendar.dateField`. It now says no alias refusal reached inside +`listViews` until that check, and that the check judges the two calendar +spellings only.