Repository navigation
Commit a243cfb
fix(lint): action-name-undefined walks the record:alert CTA and page:header action ids (#20171)
Fixes #20105
Clause-②: no
## What
`action-name-undefined`
(`packages/lint/src/validate-action-name-refs.ts`) is the authoring gate
for "a surface names an action no action in the stack defines". It
walked list-view row/bulk menus, the `record:quick_actions` bar and app
navigation, but never read the two other page surfaces that bind an
action by id:
- `record:alert` → `properties.action.actionName` (the banner's
call-to-action);
- `page:header` → `properties.actions` (the header's action ids).
Both objectui renderers resolve the id against the object's declared
actions and draw nothing on a miss (the alert keeps its banner and
silently loses its button; the header renders one button fewer and only
warns in the browser console). The spec types both as plain strings, so
a typo passed spec and lint and vanished at runtime.
This PR adds two walks to the existing rule (no new rule, no new gate),
each scoped to its component type and resolved through the same `check`
closure as `actionNames`: against every action defined in the stack,
global or object-embedded, with the same did-you-mean. Details:
- Type-scoped because the keys mean something else elsewhere:
`element:button`'s `action` is an inline definition, and
`record:related_list` declares its own `actions`.
- `page:header.actions`: only string elements are resolved, each
reported at its AUTHORED index. An inline object element is a
definition, not a reference: the spec schema rejects it, and `os
validate` already reports it as `component-props-invalid` (warning).
- `check` gained an optional consequence sentence, because "the button
renders and does nothing when clicked" is false for these two surfaces:
no button is drawn at all. The hint names each surface's placement: none
for the alert (it runs its CTA by name, with no `locations` filter),
`record_header` or `record_more` for the header (its `placedOnHeader`
filter).
- The rule header lists the two new covered surfaces.
## Mechanism readings (objectui, read-only)
- H3, `page:header.actions` shape: spec `PageHeaderProps.actions =
z.array(z.string()).describe('Action IDs to show in header')`. objectui
`PageHeaderRenderer`
(`packages/components/src/renderers/layout/containers.tsx`, same code at
the pin `f8a9d0fb05` and at `origin/main` `41ae65b2`) resolves ids ONLY
against the bound object's `actions` metadata via
`resolveDeclaredActionIds`. Host system actions (Edit / Share / Delete)
are injected separately through `RecordContext.headerSystemActions` and
never resolved from an authored id, so no built-in id can legitimately
appear here. The rule's stack-wide resolution is a superset of the
runtime's, so it refuses nothing the runtime would draw.
- `record:alert` CTA: objectui `record-alert.tsx` resolves
`action.actionName` against the object's `actions[]` (a `find` at the
pin; `resolveDeclaredActionIds` on main) and executes it through
`ActionEngine.executeAction(name)`, which applies no location filter.
## Evidence (all at `a92b55d86` unless stated)
- **Blind spot proven with failing rows first.** The test rows were
committed before the fix (`90b98e842`). Pre-fix leg: the rule file was
restored from `90b98e842` (tree only; the rule then counts
`record:alert` 0, `page:header` 0, `record:quick_actions` 2). Result:
`Tests 3 failed | 27 passed (30)`, and the three failures are exactly
the new negative rows (alert CTA; header id at index 1; header mixed
array). The restore was trapped and proven by content: the hash
`cc02f0601a96` equals the HEAD blob, and `git diff HEAD` is empty.
- **Rule file at HEAD:** `vitest run
src/validate-action-name-refs.test.ts` gives `Tests 30 passed (30)`.
- **`@objectstack/lint` full suite:** `Test Files 108 passed (108)`,
`Tests 4144 passed | 5 skipped (4149)`. `pnpm --filter @objectstack/lint
typecheck` exits 0, and its test layer holds its pinned debt. The
dependency closure was built first (`--filter '@objectstack/lint^...'`,
VERDICT command-exit 0).
- **Real metadata (H2 positive control), a one-off script not
committed:** the rule over all 45 platform-objects objects (54 embedded
actions; `sys-api-key.object.ts` was skipped, since a smaller universe
can only add findings) plus the 3 shipped pages
(`sys_organization_detail`, `sys_position_detail`, `sys_user_detail`).
- **REAL stack:** 0 findings, both pre-fix and at HEAD.
`sys_user_detail`'s `resend_verification_email` CTA resolves clean.
- **Mutation A, the real `sys_user` page with the CTA misspelled:**
pre-fix 0, HEAD 1 (`action-name-undefined` error at
`pages[0].slots.alerts[0].properties.action.actionName`).
- **Mutation B, a `page:header` with
`['resend_verification_email','resend_verification']`:** pre-fix 0, HEAD
1 (at `.properties.actions[1]`).
- **`@objectstack/platform-objects` suite** (the named live producer;
lint is aliased to source there): `Test Files 55 passed (55)`, `Tests
911 passed (911)`. None of its tests calls this rule, so this reading
shows only that nothing regressed.
- **`@objectstack/cli`:** declared to CI, not run locally. Census of its
test inputs: 1 fixture carries `page:header`
(`validate-json-strict-exit.e2e.test.ts`, with no `actions` key) and 0
carry `record:alert`, so neither new walk can yield a finding over cli's
inputs.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (derived after the changeset
landed) gave 59 commands. `--ran` reconciliation: 59 derived, 56 run
(all exit 0), 3 NOT MEASURED (exit 3, PREREQUISITE NOT MET, since each
needs a whole-workspace build that CI owns):
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt`. `check:docs-transcript-drift` first reported
PREREQUISITE NOT MET, then passed once lint was built. The derivation
flagged its tree as 4 commits behind `origin/main`: the moved files were
`dispatch-gates.mjs` (over-threshold prose only),
`check-governed-merges.mjs` and `check-governed-queue-guard.mjs`
(governed-surface landing), and none of them changes the families for
these three paths.
- **ESLint, as a proven narrowing** (the repo-wide `pnpm lint` is CI's).
Population from `eslint.config.mjs`:
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, so the changeset `.md` is
outside it. `--format json` over the 2 changed `.ts` files: 2 files, 0
errors, 0 warnings, exit 0. Invariance: the config never enables
type-aware linting (`--print-config` gives `parserOptions.project: null`
for both files), so this diff cannot move any untouched file's verdict.
## Changeset
`@objectstack/lint` gets a `patch`: a blind-spot repair to a released
rule. A stack whose alert CTA or header id names an undefined action
built clean before and now fails `os validate` / `os lint` / `os build`,
but that id never rendered a button. The rule keeps its frozen `flow`
runtime crossing, so the `page` write door is unchanged.
## Acceptance notes (noted, not filed)
- **Kebab `page-header`.** The legacy spelling (objectui
`@object-ui/layout` registration, open namespace) also reads `actions`
ids, through `record:quick_actions`. It is not walked here: the card
names `page:header`, and the census found zero in-repo producers outside
spec conversion fixtures. Carrier: none.
- **`record:related_list.actions`.** Declared as "Action IDs available
for related records", this is another by-name id list the rule does not
walk. Its runtime liveness was not measured. Carrier: none.
- **Resolution scope.** The triage note says "resolved against the page
object's declared actions", while the card and the rule's documented
posture say "exactly as `record:quick_actions` is", which is stack-wide
(zero false positives). The runtime resolves only the bound object's
`actions`, so an id defined on a different object still passes lint and
draws nothing. The same limit already applies to the quick-actions walk,
and this PR does not widen it.
- **`actionNames` index.** The existing walk reports the index into the
string-filtered list, which is off by one or more only when a non-string
element precedes. The spec refuses those elements anyway. The new header
walk reports authored indices. The existing walk is left untouched.
## Out of scope (reported to the seat for filing)
- **Class b: the published skill teaches the refused header shape.**
`skills/objectstack-ui/rules/pages.md` (the catalogue row `inline
actions: Action[]`, the example `actions: [ConvertLeadAction]`, and the
note "pass full `Action` objects into `page:header.properties.actions`")
and its eval `skills/objectstack-ui/evals/views-apps-actions-pages.json`
teach inline Action OBJECTS. The contract is ids:
`PageHeaderProps.actions: z.array(z.string()).describe('Action IDs to
show in header')`. Measured: `validateComponentProps` on that exact
shape reports `component-props-invalid` (warning) `actions.0: Invalid
input: expected string, received object`. Seam:
`spec:PageHeaderProps.actions → renderer:PageHeaderRenderer` (the
undeclared all-object transition arm).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9347c1f commit a243cfb
3 files changed
Lines changed: 179 additions & 4 deletions
File tree
- .changeset
- packages/lint/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
206 | 299 | | |
207 | 300 | | |
208 | 301 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
20 | 31 | | |
21 | 32 | | |
22 | 33 | | |
| |||
115 | 126 | | |
116 | 127 | | |
117 | 128 | | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
118 | 136 | | |
119 | 137 | | |
120 | 138 | | |
| |||
124 | 142 | | |
125 | 143 | | |
126 | 144 | | |
127 | | - | |
128 | | - | |
| 145 | + | |
129 | 146 | | |
130 | 147 | | |
131 | 148 | | |
| |||
231 | 248 | | |
232 | 249 | | |
233 | 250 | | |
234 | | - | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
235 | 254 | | |
236 | 255 | | |
237 | 256 | | |
| |||
245 | 264 | | |
246 | 265 | | |
247 | 266 | | |
| 267 | + | |
| 268 | + | |
248 | 269 | | |
249 | 270 | | |
250 | 271 | | |
251 | 272 | | |
252 | | - | |
| 273 | + | |
253 | 274 | | |
254 | 275 | | |
255 | 276 | | |
256 | 277 | | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 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 | + | |
257 | 323 | | |
258 | 324 | | |
259 | 325 | | |
| |||
0 commit comments