Skip to content

Commit 3f69471

Browse files
fix(spec): the ui-bulk-action-param-unknown-keys-refused migration text names the promoted field-backed bulk route (#22888)
Fixes #22858 Clause-②: no ## What changed Text only, in one file: `packages/spec/src/migrations/entries/semantic/18.ui-bulk-action-param-unknown-keys-refused.ts`, the major-18 D3 entry that `objectstack migrate meta` prints. Two clauses said the bulk surface has no field-backed param route. Since objectui `29b99490` (an ancestor of the console pin), a PROMOTED bulk action resolves its field-backed params through the single-record dialog's resolver. Both clauses now say that the AUTHORED `bulkActionDefs[].params` surface has no such route, name the promoted route, and name its two keys (`accept` / `maxSize` arrive from the field). The wording follows what `581ca7d4e4` landed in `packages/spec/src/ui/bulk-action.zod.ts`: the module comment at `:151`-`:170`, the `field` guidance at `:211`-`:221`, and the widget-config prescription at `:252`-`:267`. Plus a `patch` changeset for `@objectstack/spec`. No regeneration commit: `registry.ts`, `spec-changes.json` and the upgrade guide are git-ignored. `packages/spec/CHANGELOG.md` is not touched; it is release-owned. ### Before and after (evaluated strings, clause by clause; every other clause is byte-identical) **`replacement`, before:** > the keys that belong one layer out are pointed there (`visible` and a capability gate belong on the DEF, `visibleWhen` belongs on an `options[]` entry, `field` / `objectOverride` / `carryOver` / `defaultFromRow` / `requiresFeature` are field-backed ACTION-param contracts the bulk surface does not implement); and the widget-config family (…) is answered with one prescription naming `FieldSchema` as the shape those keys are real on. **`replacement`, after:** > the keys that belong one layer out are pointed there (`visible` and a capability gate belong on the DEF, `visibleWhen` belongs on an `options[]` entry, `carryOver` / `defaultFromRow` / `requiresFeature` are ACTION-param contracts the bulk surface does not implement); `field` / `objectOverride` declare a FIELD-BACKED param, and an authored `bulkActionDefs[].params` entry has no such route — it reaches the dialog as written, and nothing on that path resolves it against the object's field definitions — so the `field` rejection offers two fixes: declare the param inline, or declare the action on the object with that field-backed param and name it in the view's `bulkActions`, the PROMOTED route, whose params are resolved through the single-record dialog's resolver when the bulk dialog opens; and the widget-config family (…) is answered with one prescription naming `FieldSchema` as the shape those keys are real on, and saying that of them only `accept` / `maxSize` declared there reach this dialog, through a promoted action. **`reason`, before:** > ⛔ Do not read their rejection as "the renderer ignores them", and ⛔ do not answer it by declaring the key on the object's FIELD: the bulk surface has no field-backed param route, so that value does not reach this dialog either. **`reason`, after:** > ⛔ Do not read their rejection as "the renderer ignores them", and ⛔ do not answer it by declaring the key on the object's FIELD and keeping the authored def: an authored `bulkActionDefs[].params` entry has no field-backed param route, so that value does not reach this dialog either. The one route that consults the field is a PROMOTED action — an action the object declares, named in the view's `bulkActions`, whose field-backed params (`{ field }`) are resolved through the single-record dialog's resolver when the bulk dialog opens — and of these keys only `accept` / `maxSize` arrive that way: `lookupFilters` / `lookupColumns` / `lookupPageSize` / `descriptionField` are resolved and then dropped on the bulk path, and the rest are never read off the field. The diff also re-wraps two string-concatenation lines with no byte change to the evaluated text. That was proved by evaluating `entry` at the base `75b6d6a048` and at this head, splitting each field at clause boundaries, and diffing: only the clauses quoted above differ, and `surface` / `acceptanceCriteria` are identical. ## The claim, verified at the console pin `.objectui-sha` at the base is `4997995022` (the card was filed against `0df67f237c`; the pin has moved since). `git merge-base --is-ancestor 29b99490 4997995022` exits 0, which is self-proving even in a shallow checkout. All readings use `git show 4997995022:…` in objectui: - **The promoted route resolves field-backed params.** `packages/plugin-grid/src/ObjectGrid.tsx:5174`-`:5183`: `resolveDialogParams` calls `resolvePromotedBulkParams(def, { objectName, objects, fieldLabel, fieldOptionLabel, locale })` when the dialog opens, for a def carrying `actionDef` (a promoted one). `packages/plugin-grid/src/resolveBulkActions.ts:197`-`:212`: that function passes the action's params to `resolveActionParams` from `@object-ui/fields`, the single-record dialog's resolver, then maps each result through `toBulkParam`. - **`accept` / `maxSize` arrive.** `packages/fields/src/resolveActionParams.ts:781`-`:782` (field-backed branch): `accept: param.accept ?? field.accept`, `maxSize: param.maxSize ?? field.maxSize`. `resolveBulkActions.ts:172`-`:173`: `toBulkParam` forwards both. `packages/plugin-grid/src/components/bulkParamToField.ts:163`, `:167`: the `...extra` spread carries them onto the widget's field bag. - **`lookupFilters` / `lookupColumns` / `lookupPageSize` / `descriptionField` are resolved, then dropped.** `resolveActionParams.ts:757`, `:759`-`:761` resolve them (with the snake `id_field` read at `:756`). `toBulkParam` (`resolveBulkActions.ts:159`-`:175`) emits none of them; its docblock at `:146`-`:149` says so. - **The rest are never read.** The field-backed return (`resolveActionParams.ts:766`-`:784`, with the lookup extras at `:754`-`:762`) reads no `min`, `max`, `step`, `precision`, `scale`, `rows`, `crop`, `capture`, `dimensions`, `defaultName`, `allowCreate`, `picker`, `subtitle` or `avatarField` off the field. - **`carryOver` / `defaultFromRow` / `requiresFeature` stay unimplemented on the promoted route too,** which is why the replacement keeps that clause. `toBulkParam` forwards neither `carryOver` nor `visible` (the target that `requiresFeature` is lowered into), and the resolver context at `ObjectGrid.tsx:5177`-`:5183` passes no `row`, so `defaultFromRow` has nothing to read. `STEP18_RATIONALE` in `packages/spec/src/migrations/registry.ts.template` carries no fragment for this entry: the sorted list goes from `ui-ai-chat-window-retired` (`:2747`) to `ui-form-layout-inline-grid-retired` (`:2762`), and the template has zero `field-backed` hits. So nothing moves there. The new text carries no tracker id. `packages/cli/test/migrate-meta-engine-guidance.test.ts` refuses a hash sign followed by four or five digits in the printed block. ## Verification Every reading below is at head `30e041142c` (base `75b6d6a048`), in a fresh worktree, with each verdict read from the tool's own line. - **`@objectstack/spec` build** (lock): `VERDICT command-exit 0`. It runs `gen:migration-registry` (`build-migration-registry --self-test: ok`, wrote 422 semantic entries). The new text is in the generated `registry.ts` (git-ignored) and in `dist/migrations` (one hit each), and the old sentence has zero hits. The build left `git status` clean. - **`@objectstack/spec` test** (`local` project): 648 files, 19375 passed, 1 todo, exit 0. **typecheck**: exit 0 (`tsc --noEmit`, `check:scripts-typecheck`, `check:test-typecheck` OK). - **`scripts/step18-rationale-merge.test.ts`** (`repo` project, the one `repo` test that reads the template): 9/9 passed, exit 0. - **CLI closure build** `turbo run build --filter='@objectstack/cli^...' --concurrency=2`: 58/58 tasks successful, `VERDICT command-exit 0`. - **`packages/cli/test/migrate-meta-engine-guidance.test.ts`** (`--project integration`, after that build): 3/3 passed, exit 0. It spawns `os migrate meta --all`, finds every semantic entry's block verbatim in stdout (this one included), and refuses a tracker id in it. - **`node scripts/check-adr-0087-registration.mjs --base origin/main`**: `this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)`. - **`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`** (no paths): 82 commands, of which 81 were run, all exit 0. Three of them first answered `PREREQUISITE NOT MET` (exit 3: `doc-formula-expressions`, `dts-closure`, `lean-entry-closure`, which need built workspace packages). They were re-run after the CLI closure build and exit 0. `--ran` reconciliation: `82 derived, 81 run, 0 NOT-MEASURED, 1 UNRUN`. - The UNRUN one is `pnpm check:dual-build-cjs-loads`: NOT MEASURED, reason: it needs a whole-workspace build, which this dispatch rules out. - The spec gates that read `dist` (`check:api-surface`, `check:authorable-surface`, `check:docs`, `check:spec-changes`, `check:upgrade-guide`, `check:liveness`, `check:strictness-ledger` and the rest, 18 in all) ran after the spec build. `check:authorable-surface` prints only the allowed `baseRev`-lag info line. - **Artifact rosters** (the 50 families the no-path run lists, PR-context guards wired to this PR): run after this PR opened, because the PR-context guards read its number and body. Their results are in the `os-dev-report` comment on the card, not here: this body is written once. - **NOT MEASURED**: the five path-scheduled CI jobs and the four type-check lanes the derivation names as CI-only (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core; the workspace typecheck filters). Reason: they are CI's own shell over CI's filters, with no local invocation. - **Not merged with `origin/main`**: two commits have landed since the base (`72b26ed4f5` and `40bc51f3f9`). Neither touches either path in this diff. The PR's merge ref and the merge queue test the joint state. ## Acceptance notes - **`acceptanceCriteria` is left untouched, as ruled.** Its closing sentence still says that where a widget-config key's behaviour is gone, "the remedy is a spec issue asking for the key, not a local workaround". For `accept` / `maxSize` there is now a second remedy: the landed prescription's "for `accept` / `maxSize` move the param onto a promoted action". The `reason` clause printed just above it now states that remedy, so the printed block is not wrong, only incomplete in its last sentence. Whether to align that sentence is a call for the reviewing seat. Carrier: the seat reviewing this PR. Noted here; no card is filed. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6373952 commit 3f69471

2 files changed

Lines changed: 40 additions & 11 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the `ui-bulk-action-param-unknown-keys-refused` upgrade guidance names the promoted field-backed bulk route instead of saying the bulk surface has none (#22858)
6+
7+
Clause-②: no
8+
9+
The major-18 semantic entry `ui-bulk-action-param-unknown-keys-refused`, which `objectstack migrate meta` prints, said two things that stopped being true once the console resolved a promoted bulk action's params through the single-record dialog's resolver. It called `field` / `objectOverride` contracts "the bulk surface does not implement", and it told the reader that declaring a widget-config key on the object's field cannot help because "the bulk surface has no field-backed param route". The entry now uses the wording the `BulkActionParamSchema` rejection messages already use.
10+
11+
- **`replacement`.** An authored `bulkActionDefs[].params` entry has no field-backed route: it reaches the dialog as written. The `field` rejection offers two fixes: declare the param inline, or declare the action on the object with the field-backed param and name it in the view's `bulkActions`. That second form is the promoted route, whose params are resolved when the bulk dialog opens. Of the widget-config keys, only `accept` / `maxSize` declared on the field reach the dialog, through a promoted action. `carryOver` / `defaultFromRow` / `requiresFeature` are still ACTION-param contracts the bulk surface does not implement.
12+
- **`reason`.** Declaring the key on the field and keeping the authored def still does not reach the dialog. A promoted action is the one route that consults the field. Of the refused keys, only `accept` / `maxSize` arrive that way. `lookupFilters` / `lookupColumns` / `lookupPageSize` / `descriptionField` are resolved and then dropped on the bulk path, and the rest are never read off the field.
13+
14+
Nothing to migrate: the accepted and refused keys are unchanged. Only the guidance text of one migration entry moved.

‎packages/spec/src/migrations/entries/semantic/18.ui-bulk-action-param-unknown-keys-refused.ts‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,22 @@ export const entry: SemanticMigration = {
2222
+ '`help`, `defaultValue` → `default`, `reference` → `object`, `displayField` → '
2323
+ '`labelField`); the keys that belong one layer out are pointed there (`visible` and a '
2424
+ 'capability gate belong on the DEF, `visibleWhen` belongs on an `options[]` entry, '
25-
+ '`field` / `objectOverride` / `carryOver` / `defaultFromRow` / `requiresFeature` are '
26-
+ 'field-backed ACTION-param contracts the bulk surface does not implement); and the '
27-
+ 'widget-config family (`min`, `max`, `step`, `precision`, `scale`, `rows`, `accept`, '
28-
+ '`maxSize`, and the picker knobs `lookupFilters` / `lookupColumns` / `lookupPageSize` / '
25+
+ '`carryOver` / `defaultFromRow` / `requiresFeature` are ACTION-param contracts the bulk '
26+
+ 'surface does not implement); `field` / `objectOverride` declare a FIELD-BACKED param, '
27+
+ 'and an authored `bulkActionDefs[].params` entry has no such route — it reaches the '
28+
+ 'dialog as written, and nothing on that path resolves it against the object\'s field '
29+
+ 'definitions — so the `field` rejection offers two fixes: declare the param inline, or '
30+
+ 'declare the action on the object with that field-backed param and name it in the '
31+
+ 'view\'s `bulkActions`, the PROMOTED route, whose params are resolved through the '
32+
+ 'single-record dialog\'s resolver when the bulk dialog opens; and the widget-config '
33+
+ 'family (`min`, `max`, `step`, `precision`, `scale`, `rows`, `accept`, `maxSize`, and '
34+
+ 'the picker knobs `lookupFilters` / `lookupColumns` / `lookupPageSize` / '
2935
+ '`descriptionField` / `picker` / `subtitle` / `avatarField` / `idField` / `allowCreate`) '
3036
+ 'is answered with one prescription naming `FieldSchema` as the shape those keys are real '
31-
+ 'on. `dependsOn` needs NO edit — it is declared, in the same shape the field-level key '
32-
+ 'takes (`[\'parent\']`, or `[{ field, param }]` when the remote filter key differs).',
37+
+ 'on, and saying that of them only `accept` / `maxSize` declared there reach this dialog, '
38+
+ 'through a promoted action. `dependsOn` needs NO edit — it is declared, in the same '
39+
+ 'shape the field-level key takes (`[\'parent\']`, or `[{ field, param }]` when the remote '
40+
+ 'filter key differs).',
3341
reason:
3442
'The accept was a NULL READING, and that is what makes this a contract fix rather than a '
3543
+ 'preference. Measured against installed spec 17.4.0, three parses per schema in one '
@@ -51,11 +59,18 @@ export const entry: SemanticMigration = {
5159
+ 'accepted cost of closing the shape (maintainer ruling, letter '
5260
+ 'A, 2026-09-17: 「Breaking for authored metadata」, one-shot, no grace window and no dual '
5361
+ 'spelling). ⛔ Do not read their rejection as "the renderer ignores them", and ⛔ do not '
54-
+ 'answer it by declaring the key on the object\'s FIELD: the bulk surface has no '
55-
+ 'field-backed param route, so that value does not reach this dialog either. A census of '
56-
+ 'authored bulk params taken at registration time over the two repositories reachable from '
57-
+ 'that session found ZERO carrying an undeclared key (objectstack@176b03582e: 7 param '
58-
+ 'literals; objectui@3e4f6324f7: 3), so no in-corpus configuration is known to break. '
62+
+ 'answer it by declaring the key on the object\'s FIELD and keeping the authored def: an '
63+
+ 'authored `bulkActionDefs[].params` entry has no field-backed param route, so that value '
64+
+ 'does not reach this dialog either. The one route that consults the field is a PROMOTED '
65+
+ 'action — an action the object declares, named in the view\'s `bulkActions`, whose '
66+
+ 'field-backed params (`{ field }`) are resolved through the single-record dialog\'s '
67+
+ 'resolver when the bulk dialog opens — and of these keys only `accept` / `maxSize` arrive '
68+
+ 'that way: `lookupFilters` / `lookupColumns` / `lookupPageSize` / `descriptionField` are '
69+
+ 'resolved and then dropped on the bulk path, and the rest are never read off the field. '
70+
+ 'A census of authored bulk params taken at registration time over the two repositories '
71+
+ 'reachable from that session found ZERO carrying an undeclared key '
72+
+ '(objectstack@176b03582e: 7 param literals; objectui@3e4f6324f7: 3), so no in-corpus '
73+
+ 'configuration is known to break. '
5974
+ '⚠️ That census did NOT cover hotcrm, which was unreachable from the session that took it '
6075
+ '— that leg is UNMEASURED, not clean, and an upgrader with their own metadata corpus '
6176
+ 'should run the check below rather than inherit this result.',

0 commit comments

Comments
 (0)