Repository navigation
Commit 0fc8087
fix(lint)!: action-name-undefined resolves record:related_list action ids against the child object (#21626)
Fixes #20936
Clause-②: no (narrowing)
## What
`action-name-undefined`
(`packages/lint/src/validate-action-name-refs.ts`) now walks
`record:related_list` → `properties.actions[]`, scoped to that component
type, and asks the two questions the console's renderer asks:
1. Is the id an action of the related list's **child** object? The
child's actions are the ones written on that object plus every
`stack.actions` entry bound to it by `objectName` (the set `defineStack`
merges into the object's `actions`, so the set the object's metadata
serves). An id defined only on the page's object, or only as a global
action, is refused like a typo. The message names where it is defined.
The did-you-mean and the hint's action list come from the child object.
2. Does that action declare a location a related list draws? The
location set is read from the spec's `ACTION_LOCATIONS` (imported from
`@objectstack/spec/ui`), classified per member in a `Record` keyed by
the spec's `ActionLocation` type. That gives `list_toolbar`, `list_item`
and `record_related`. A location the spec adds fails
`@objectstack/lint`'s typecheck until it is classified, so the set
cannot go stale the way a hard-coded pair would.
Both findings use the same rule id, `action-name-undefined`, at severity
`error`. Each is reported at the id's authored index. Only string
elements are ids (inline objects are skipped, as on `page:header`). The
child object is the component's bound `dataSource.object` when one is
set, otherwise `properties.objectName`. The walk says nothing about a
child object this stack does not define.
## Measurements against the PM's mechanism assumptions
Read at `origin/main` `f97660cdd6` (this branch's base) and at objectui
`89cad75d55702cc4f267bead5bf267de575d5842` (`.objectui-sha` on that
base).
1. **Holds.** The rule walked `record:quick_actions.actionNames[]`,
`record:alert`'s `action.actionName` and `page:header`'s `actions[]`.
The docblock at `:27`–`:30` said `actions` "is declared separately on
`record:related_list`", and no walk read it. That sentence is replaced
by a bullet for the new walk.
2. **Holds.** At the pin,
`packages/plugin-detail/src/renderers/record-related-list.tsx:287` reads
`schema.actions`. `:290` looks up `useMetadataItem('object', …)` for the
related `objectName`. `:301` takes `relatedObjectMeta.actions` as the
registry, and `:303` hands both to `placeAuthoredRelatedListActions`. In
`relatedListActions.ts`, `:110` resolves the ids with
`resolveDeclaredActionIds` against that registry. `:141`–`:143` place by
`actionRendersAt` at `list_toolbar` / `list_item` / `record_related`. An
id that resolves but is placed at none of the three is refused as
`unplaced`. Control: at the previous pin `db11afd4967c`, `git grep -c
"schema.actions"` on the same file answers 0 (exit 1), while
`schema.relationshipField` answers 3, so the grep reaches the file. `git
merge-base --is-ancestor f4ed2387e9 89cad75d5570` exits 0.
3. **No existing walk scopes to an object.** Every walk resolves through
`collectActionNames`, the union of `stack.actions` and every object's
`actions`. The related list's child is `properties.objectName`, the same
key `validate-page-field-bindings.ts`'s `relatedListFieldRefs` already
reads. objectui's data-source gate
(`packages/react/src/element-data-source/ElementDataSourceGate.tsx:398`)
writes a bound `dataSource.object` over it. How the scope note and the
direction fit: the note keeps the other walks stack-wide because
ownership and location checks there would cost the ADR-0072 D1
zero-false-positive posture for coverage nobody asked for. Neither
reason holds for this walk. The renderer asks both questions itself and
refuses on either miss, so a finding is the runtime's own verdict moved
to authoring time ("resolve at runtime for the surface being authored",
ADR-0072 D1). This card's ruling also asks for that coverage. One
false-positive source stays: a child object this stack does not define
has its actions in another package, so the walk is silent there rather
than guessing. The docblock's scope note now says this, and every other
walk is unchanged. I see no real conflict, so this is not raised as a
question.
4. **Partly disproved.** `ACTION_LOCATIONS`
(`packages/spec/src/ui/action.zod.ts:663`) is the full six-location
vocabulary and lists `record_related`. It does not declare which
locations a related list draws: the list draws three of the six. Using
the whole vocabulary as the set would accept a `record_header`-only
action that the renderer refuses. So the rule imports the vocabulary and
the `ActionLocation` type and classifies every member exhaustively. No
location literal is copied outside a compiler-checked key. Whether the
spec should export that subset itself is in the report's
`open_questions`. This PR does not decide it.
5. **Holds.** The landing site is `packages/lint`. There is no
producer-side change: the spec already types the key as
`z.array(z.string())`, and the renderer already refuses.
## Pins (beside #20105's)
`validate-action-name-refs.test.ts`, `describe('validateActionNameRefs —
record:related_list actions')`:
- ids that resolve on the child at `record_related`, `list_toolbar`,
`list_item`, plus one bound through `stack.actions` `objectName`:
**silent**;
- an id that resolves nowhere: one finding at its authored index, naming
the id and the child object;
- **decision pinned:** an id defined only on the page's object, and one
defined only as a global action: both are findings, because the list
never reads either;
- child actions placed at `record_header` only, at `[]`, and with no
`locations`: three findings;
- a bound `dataSource.object` wins over `objectName`;
- a child object this stack does not define (`sys_member`): **silent**.
## Tests (all at `b37812bed9` unless noted)
- `pnpm --filter @objectstack/lint test`: `Test Files 119 passed (119)`,
`Tests 5621 passed | 5 skipped (5626)`.
- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2
src/validate-action-name-refs.test.ts`: `Tests 36 passed (36)` (30
before, plus 6).
- `pnpm --filter @objectstack/lint typecheck` (at `6a73a0ad67`, same
`src` as the head): exit 0. The test layer is clean against its ledger:
"2 file(s) / 6 error(s) / 2 pinned signature(s) held", unchanged.
- Built-`dist` probe:
`require('packages/lint/dist/index.cjs').validateReferenceIntegrity(stack)`
on a related list naming one defined and one undefined child id returns
exactly one `action-name-undefined` `error` at `…properties.actions[1]`.
The ESM entry gives the same count. This is the suite `os validate` /
`os lint` / `os build` call.
- Corpus: there was no fixture to re-read. The platform's pages
(`sys-user`, `sys-organization`, `sys-position`, walked with
`walkPageComponents`) hold 13 related lists, and 0 of them author
`actions`. `git grep -c related_list -- examples` (CHANGELOGs excluded)
answers 0 files, exit 1. Control: `record:quick_actions|page:header`
hits 3 files in the same tree.
## Reverse verification (one-off, from the committed head, restore
proven by blob equality and an empty `git diff HEAD`)
- Deleting `record_related` from the classification (`node
scripts/ablation-replace.mjs … --delete -- pnpm --filter
@objectstack/lint exec tsc --noEmit`): red with `TS2741: Property
'record_related' is missing in type …`. Restored to blob `e0deed9272da`,
which equals HEAD.
- Setting `record_related: null` and running the rule's test file:
`Tests 4 failed | 32 passed (36)`. All four new tests that use a
`record_related` action go red; the stack-wide walks stay green.
Restored to blob `e0deed9272da`, which equals HEAD.
## Gates
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `b37812bed9`, and reconciled with
`--ran`: "62 derived famil(ies) accounted for — 60 run, 2 NOT-MEASURED".
Every run family exited 0, including `check-adr-0087-registration` (the
changeset carries `not-required (no-migration-prescription)`),
`check-changeset-no-major`, `check-empty-changeset`, `check:nul-bytes`,
`check:doc-authoring`, `check:engine-double-contract` and
`check:type-check-debt` (re-run after a first attempt hit my own 300s
timeout under box contention; the re-run reported "none above its
recorded number").
- NOT MEASURED: `check:dual-build-cjs-loads` and
`check:lean-entry-closure`, reason: PREREQUISITE NOT MET, because they
need the whole workspace built (85 packages have no `dist/`). Narrowed
instead: the diff adds no new import specifier to `@objectstack/lint`
(`@objectstack/spec/ui` was already imported 17 times). After `pnpm
--filter @objectstack/lint build`, the CJS `require` of `dist/index.cjs`
and the ESM import both load and run the rule. CI runs both gates over
the full build.
- `check:docs-transcript-drift` first exited 3 (lint unbuilt). It exited
0 after the lint build.
- ESLint, narrowed and proven. ① Population, from `eslint.config.mjs`:
both files match `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and
`packages/**/*.{ts,tsx,mts,cts}`, and neither is under `NEVER_LINTED`. ②
`npx eslint --no-inline-config --format json` on the two changed source
files: 2 files, 0 errors, 0 warnings. ③ The config never enables
type-aware linting (`eslint.config.mjs:327`–`:328`: no
`parserOptions.project`, no typed rules), and every block is per-file
syntactic, so this diff cannot move any untouched file's verdict.
## Acceptance notes
- **One rule id for both refusals.** An id that resolves to a child
action placed nowhere the list draws is reported as
`action-name-undefined`, with a message that says it IS defined and
names its `locations`. A new rule id would be a new public export from
`@objectstack/lint` (a widening), and the claim declares `Clause-②: no
(narrowing)`.
- **Docs drift, not fixed here.** `content/docs/ui/actions.mdx:276`
still says `record_related` is "Declared, not yet placed: the console
does not draw it on those rows yet". At the pin, the related list does
draw it (`relatedListActions.ts:143`; objectui#11270 merged as
`a8b9889332`, behind the pin by 0). The same page's "Surfaces can also
reference actions by name" list (`:279`) does not list
`record:related_list.actions`. Neither is in this card's file surface.
Carrier: none named.
- The rule still does not run at the runtime publish door for `page`
writes: its suite member keeps the default `flow` runtime type.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a4f0cb0 commit 0fc8087
3 files changed
Lines changed: 301 additions & 3 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
296 | 296 | | |
297 | 297 | | |
298 | 298 | | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
299 | 391 | | |
300 | 392 | | |
301 | 393 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
30 | | - | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
31 | 37 | | |
32 | 38 | | |
33 | 39 | | |
| |||
51 | 57 | | |
52 | 58 | | |
53 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
54 | 71 | | |
55 | 72 | | |
| 73 | + | |
56 | 74 | | |
57 | 75 | | |
58 | 76 | | |
| |||
102 | 120 | | |
103 | 121 | | |
104 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
105 | 199 | | |
106 | 200 | | |
107 | 201 | | |
| |||
111 | 205 | | |
112 | 206 | | |
113 | 207 | | |
| 208 | + | |
114 | 209 | | |
115 | 210 | | |
116 | 211 | | |
| |||
248 | 343 | | |
249 | 344 | | |
250 | 345 | | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
251 | 421 | | |
252 | 422 | | |
253 | | - | |
| 423 | + | |
| 424 | + | |
254 | 425 | | |
255 | 426 | | |
256 | 427 | | |
| |||
320 | 491 | | |
321 | 492 | | |
322 | 493 | | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
323 | 507 | | |
324 | 508 | | |
325 | 509 | | |
| |||
0 commit comments