Repository navigation
Commit 88a39c0
Fixes #21855
Clause-②: yes (narrowing)
## What this does
An `action:group` / `action:menu` member's `params` now takes the array
form (`ActionParam[]`, the input list) only, unless the member's `type`
is `api`. Any other `params` value on a non-`api` member (an object, a
string, a number or `null`) is refused at the component-props gate, at
`actions.N.params`, with the member prescription in the file's existing
voice: static values belong on an `action:button` node, whose `params`
object carries them.
This is the card's step, under the governing text it cites:
- The maintainer's ruling A on objectstack-ai/objectui#10289
(`5825589480`), verbatim: "⛔ `params` never carries two shapes, and ⛔ no
new value-bag key is declared."
- #21704 fork 5 A (`5979239990`) refused a
member's `properties.params` and kept the member's `params` as
`z.unknown()`, the button row's value schema.
- The member's own `properties` prescription in `component.zod.ts`
already pointed at `action:button` for static values.
- triage's answer on objectstack-ai/objectui#11638 (`5990495682`,
amended by `5991243780`) withdrew the other direction (the container
reading an object `params`) and filed this card for the gate half.
| | before | after |
|:--|:--|:--|
| a non-`api` member, `params` an array | accepted | accepted,
byte-identical |
| a non-`api` member (an absent `type` included), `params` not an array
| accepted, then dropped at run time | refused: `custom` at
`actions.N.params`, with the prescription |
| a `type: 'api'` member, any `params` | accepted (the request-payload
window, to 18) | **unchanged** |
| `action:button` / `action:icon` node, object `params` | accepted (its
static values) | **unchanged** |
The refusal message, for a `navigate_edit` menu member:
```text
`params` on an `action:menu` member is the list of inputs the runner collects from the user before the action runs — an `ActionParam[]` array. The container forwards any other `params` value only for a `type: 'api'` member, as its request payload (write `bodyExtra` for that), and this member's `type` is `'navigate_edit'`, so the object written here is dropped and never reaches the action. A member's static parameter values are not part of the inline action vocabulary: to run an action with static parameter values, author it as its own `action:button` node, whose `params` object carries them.
```
### How
- **`packages/spec/src/ui/component.zod.ts`.**
- A module-private refinement,
`actionContainerMemberParamsFitType(container)`, goes on both member
builders (`buildActionGroupMember`, `buildActionMenuMember`) through
`.superRefine`.
- The member's `params` stays `z.unknown()`, so it keeps the enumeration
pin's `runner` line. Its `.describe()` now states the accept set and
still says "forwarded to the runner".
- The members' docblock gains a section with the read points at the
`.objectui-sha` pin `0abd4f9f8`. The objectui renderer directory is
byte-identical there, at `2e818d0b5` and at objectui `main` `f1a177c41`
(`git diff --quiet`).
- `strictObject`'s unknown-key refusal stays terminal, so a member
already refused for a key is not judged a second time (pinned).
- ⛔ **What stays the same:** no key added or removed, no second shape
for `params`, no export moved, and the `api` window is untouched.
### The ADR-0087 kit (the #21702 / PR #21712 and #21464 / PR #21764
shape)
- The D3 semantic entry
`packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts`.
- Its `STEP18_RATIONALE` fragment at **order 83**, inserted where its id
sorts. 83 is the next free order: the highest on `main` is 82, re-read
at `5b2d189e28` and again at `2df3d13d16` after the merge.
- `registry.ts`'s generated region, written by `gen:migration-registry`.
- One BREAKING `@objectstack/spec` `minor` changeset,
`.changeset/21855-action-member-params-array-only.md`. It carries the
`Clause-②` line, the `adr-0087: registered
ui-action-group-menu-member-params-array-only` disposition marker and a
FROM → TO table.
- No tombstone, because no key is removed. No D2 conversion, because the
static values belong on a different node, which no rewrite can build in
the author's place.
- **No regeneration is owed for `spec-changes.json` and
`docs/protocol-upgrade-guide.md`.** Both project the registry from the
support floor up to the current protocol major (17), so a step-18 entry
is not in either yet. `check:spec-changes` and `check:upgrade-guide` are
green with both files untouched, as on the two precedents.
- **`packages/spec/dropped-refinements.baseline.json`** gains
`ui/ActionGroupProps` and `ui/ActionMenuProps` (site `actions.element`
each), exactly as `build-schemas` printed them. Its `measured` counts go
218 → 220 schemas and 676 → 678 sites. The JSON Schema projection has no
arm for a `custom` check; the S-final stage declared its timeline
refinement the same way.
- **`content/docs/references/ui/component.mdx`** was regenerated by
`check:generated --fix`. That run proved this the only stale artifact;
the change is the two member `params` rows.
## Census, re-run before writing (at base `5b2d189e28`), with a lit
control
The question: which `action:group` / `action:menu` members author a
non-array `params` on a non-`api` type?
**Instrument** (scratchpad `census.cjs`):
- A TypeScript-AST walk over
`.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`/`.mts`/`.json` and fenced
Markdown code. YAML is read as text.
- Pass 1 lists every `params` key in every file that names either block,
with its value kind and its owner's `type`. Same-file constants are
resolved. A member is classified automatically when its `actions` owner
(flat, inside a node's `properties` bag, or through a same-file constant
array) carries the block `type`.
- Pass 2 lists every non-array `params` on an element of any `actions`
array corpus-wide, to catch members built in files that never name a
block.
- Every non-array hit was read by hand. Helper positions
(`mount(surface, entry)`, `schema(member({ … }))`) are resolved that
way.
- **Lit control:** a planted fixture with four non-`api` object members
(flat, inside `properties`, through a const array, in a Markdown fence),
one `api` member and one array member. The instrument found and
classified all six. Separately, the hand-read loose pass reached
objectui's own helper-position drop probes, below.
| corpus | files | naming a block | `params` keys there | not an array |
container members with a non-array `params` on a non-`api` type |
|:--|--:|--:|--:|--:|:--|
| objectstack `5b2d189e28` | 10069 | 24 | 32 | 23 | **0**. The 23 are
inline `element:button` action conversion fixtures, schema source, the
liveness ledger's `params` row, CHANGELOG quotations, and one
`properties.params` refusal probe. |
| objectui pin `0abd4f9f8` | 7451 | 89 | 47 | 41 | **0 writers.** The
only such members are objectui's own tests asserting that the container
drops the value: `action-entry-object-params-10462.test.tsx:144`, `:193`
and `action-container-member-params-10290.test.tsx:196`. The `type:
'api'` controls beside them stay accepted. |
| objectui `main` `f1a177c41` | 7467 | 89 | 47 | 41 | the same; zero
hits differ between pin and main |
| hotcrm `4054ec2680` | 888 | 0 | 0 | 0 | **0** |
| cloud | — | — | — | — | **not reachable** from this session: `git
ls-remote` asks for credentials, the API answers 403, and `add_repo`
(read) answers "you don't have access" |
Deployed metadata was not measured. So the change narrows a zero-writer
spelling, and `Clause-②: yes (narrowing)` holds.
## Tests
- **New pin**
`packages/spec/src/ui/component-action-member-params-array-only.pin.test.ts`,
39 tests:
- §1: the refusal on both containers, each case asserting `[{ code:
'custom', path: 'actions.N.params' }]` exactly. The values are an object
on `navigate_edit`, on an absent `type` and on `url`; an empty object; a
string; a number; and `null`. One more case puts the issue on the second
member. The message names the container, the member's `type` and the
`action:button` prescription.
- §2: the accept set, byte-identical. That is an array on non-`api` and
`api` members, an empty array, the `api` member's object and string
`params`, no `params`, and `bodyExtra`. A CONTROL shows `action:button`
/ `action:icon` nodes keep their object `params`.
- §3: one complaint only, because the unknown-key refusal is terminal.
- §4: the D3 entry is registered with no conversion, and the step-18
rationale names it.
- **Ablation, predicted first.** Prediction: 18 red (all of §1), 21
green in the pin, and the two neighbour pins untouched.
- Mutation, at `7a28c5194a` (the `component.zod.ts` blob is unchanged
since, through the merge): `scripts/ablation-replace.mjs` replaced the
refinement's guard with a bare `return` (anchor ×1 → ×0, blob
`d8565fd7a8` → `930000300e`), inside a driver carrying its own `EXIT INT
TERM` restore trap on the absolute path.
- Observed over the three pins: `Tests 18 failed | 160 passed (178)`.
The 18 were exactly the §1 cases.
- Restore: blob `d8565fd7a8` equals HEAD's, and `git diff HEAD` is
empty.
- The pin imports `./component.zod` relatively, so it reaches `src` and
no `dist` is involved.
- **The public door.** lint's `validateComponentProps`, from `src`, ran
over this branch's built `@objectstack/spec`:
- a `navigate_edit` group member and a menu member with no `type`, each
with an object `params`, each give one `component-props-invalid` finding
(`warning`) at
`pages[0].regions[0].components[0].properties.actions.0.params`,
carrying the message above;
- CONTROLS give 0 findings: an array on a `script` member, an object on
an `api` member, and an object on an `action:button` node.
- The before-state is the card's own reading: the door reported nothing
for such a member.
## Verification
All at head `810b1c4b18` (the merge of `main` `2df3d13d16`, below)
unless noted.
- **`@objectstack/spec`, `vitest run --project local --maxWorkers=2`**
(the package's `test` script), under the verify lock: `Test Files 616
passed (616)`, `Tests 18426 passed | 1 todo`. Before the merge, at
`a6f230772f`, both projects (`local` and `repo`): `Test Files 669 passed
(669)`, `Tests 19325 passed | 1 todo`.
- **`pnpm --filter @objectstack/spec typecheck`**, run at `a6f230772f`:
exit 0. That covers `tsc --noEmit`, `check:scripts-typecheck` and
`check:test-typecheck: OK` (52 files / 246 errors / 135 signatures held,
unchanged). `tsc -p tsconfig.test.json --listFilesOnly` names the new
pin, and the D3 entry is in the `tsconfig.json` program.
- **`@objectstack/lint`, whole suite**, the component-props gate's
package: `Test Files 119 passed (119)`, `Tests 5627 passed`.
- **`@objectstack/spec` build, then `check:generated`**: `All 15
generated artifacts are up to date`. The `check:generated --fix` run
before the merge proved `check:docs` the only stale artifact, and only
it was regenerated.
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` (no paths) derived 114 commands at
`810b1c4b18`, against merge base `2df3d13d1` (7 paths). Each was run
with its exit code recorded before any pipe. `--ran` reconciled **114
derived, 114 run, 0 NOT-MEASURED, 0 UNRUN**, and all 114 exited 0.
- Six first answered `PREREQUISITE NOT MET` (exit 3) because workspace
packages were unbuilt: lint's `check:doc-formula-expressions` and
`check:doc-security-posture`, spec's `check:skill-examples`,
`check:docs-transcript-drift`, `check:dual-build-cjs-loads` and
`check:lean-entry-closure`. After `turbo run build
--filter=!@objectstack/docs --concurrency=2` they were re-run at the
same head, each reaching its own verdict with exit 0. The record holds
the re-runs.
- That build reported `@objectstack/hono#build` failed in its
DTS-emitted check. The `dist/index.d.ts` it named missing was on disk
right after, and a rebuild was green (`31 successful, 31 total`). The
adapter is outside this diff.
- Among the 114: `check-adr-0087-registration --base origin/main` (one
declared-breaking changeset, `registered
ui-action-group-menu-member-params-array-only`),
`check-changeset-no-major`, `check-empty-changeset`,
`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`,
`check:authorable-surface`, `check:api-surface`, `check:docs`,
`check:strictness-ledger`, `check:doc-authoring`,
`check:issue-citations`, `check:cross-package-test-inputs`,
`check:nul-bytes`, `check:type-check-debt`.
- **eslint, narrowed and proven.** These three together make the
narrowing a measurement:
1. Population: `eslint.config.mjs`'s
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block covers all four changed
`.ts` files.
2. `eslint --no-inline-config --format json`: 4 files, 0 errors, 0
warnings.
3. Invariance: the config enables no type-aware linting (no
`parserOptions.project`, as its own comment states), so this diff cannot
move a verdict on an untouched file.
- **Merge.** `origin/main` moved 4 commits after the base (`5b2d189e28`
→ `2df3d13d16`). It was merged through `scripts/pm/os-regen-merge.sh` (⛔
no rebase), and none of those commits touches a file of this PR. A
re-fetch just before this PR showed 4 more commits on `main`, none
touching these files, so no second merge.
- **Not measured here:** the full `pnpm lint`, the Console Pin Gate, the
Dogfood Regression Gate and the `repo` vitest project after the merge.
Reason: CI-owned.
## Acceptance notes
- **Interpretation, stated:** "array form only" is read literally. A
string, a number or `null` `params` on a non-`api` member is refused as
well as an object. The container drops each of those the same way
(`readActionEntryParamValues` returns nothing for any non-array value on
a non-`api` type), and the census found none of them either. If the
reviewer reads the card as objects only, the change is one condition in
the refinement's guard plus the string, number and `null` cases in §1.
- **The `api` window is untouched, and ending it is not this PR's job.**
An `api` member's object `params` stays accepted. When protocol 18 ends
that window, the member refinement's `type === 'api'` arm is the one
line that moves. Carrier: whoever ends that window. Noted, not filed.
- **objectui remainder, already carried.** `readMemberStaticParamValues`
still reads a member's `properties.params`, which the spec refuses.
objectstack-ai/objectui#11638, as re-scoped by triage (`5991243780`),
carries it. No objectui file is touched here.
- **Inert number in a hand-edited ledger.**
`dropped-refinements.baseline.json`'s
`measured.refinementSitesThatDidProject` reads 369, while this build
prints "450 refinement site(s) DID reach the file". No gate reads that
number. It is left as found; changing it is outside this card. Carrier:
none. Noted, not filed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent aead296 commit 88a39c0
7 files changed
Lines changed: 387 additions & 8 deletions
File tree
- .changeset
- content/docs/references/ui
- packages/spec
- src
- migrations
- entries/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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
173 | | - | |
| 173 | + | |
174 | 174 | | |
175 | 175 | | |
176 | 176 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
6 | | - | |
| 5 | + | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
1203 | 1203 | | |
1204 | 1204 | | |
1205 | 1205 | | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
1206 | 1216 | | |
1207 | 1217 | | |
1208 | 1218 | | |
| |||
Lines changed: 47 additions & 0 deletions
| 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 | + | |
0 commit comments