Repository navigation
Commit c0ae356
fix(lint)!: refuse a page component whose properties is present but not an object, nested ones included (#25067)
Closes #24991
Clause-②: no (narrowing: `os lint --strict` refuses a nested page
component whose `properties` is `null` or an array)
## What
`validateComponentProps`
(`packages/lint/src/validate-component-props.ts`) now refuses a walked
page component, top-level or nested, whose `properties` is present but
not a record (`null`, an array, a scalar). The refusal is one
`component-props-invalid` error at the node's own `properties` path, for
example
`pages[0].regions[0].components[0].properties.children[0].properties`.
The message names what was received.
- It applies whatever the node's `type`, registered or not: the bag's
shape is the node's contract, not a props schema's. An unregistered type
with an object bag is still skipped, as before.
- No per-prop "missing required" verdicts are added beside it. A
malformed bag yields one finding, not one per required key.
- An absent bag keeps its earlier verdict (judged as `{}`), unchanged
from the absent-bag fix in #22212.
- The binding verdict for data-source-bound elements is independent of
the bag and still fires alongside.
## Why
`PageComponentSchema` declares `properties` a record, but that parse
only reaches top-level components (`regions[].components[]`, `slots.*`).
A nested component (in `children`, `items[].children` or `footer`) sits
in a `z.array(z.unknown())` slot that no parse judges. The rule set
`props` to `undefined` for a non-record bag and skipped the node, so a
nested `record:related_list` with `properties: null` or `[]`, or a
nested `object-metric` with `properties: []`, passed `os lint --strict`,
`os validate` and `tsc`. The node published with none of its props.
## No duplicate with the schema's own top-level refusal
At both doors (CLI and the runtime page save door), the page parse runs
before any authoring rule and stops on its own refusal. So a top-level
non-record bag is reported once, by the parse ("expected record"), and
this rule's finding never appears beside it. On a top-level node the new
branch only answers a caller that hands the rule an unparsed stack, and
it gives the same verdict. A test pins both halves:
`PageSchema.safeParse` refuses the top-level bag at
`regions.0.components.0.properties` and accepts the nested one, which is
the gap.
## Tests
On head `88d3dd0572` (the lint source is byte-identical to `c444510a55`,
where the package suite ran; the later merge brought no `packages/lint`
or `packages/spec` change):
- `pnpm --filter @objectstack/lint test`: 153 files passed, 7080 tests
passed, 5 skipped.
- `pnpm --filter @objectstack/lint typecheck` (with
`check:test-typecheck`): exit 0.
- New cases cover five walks (container `children`, tab
`items[].children`, accordion `items[].children`, card `footer`, a slot
component's `children`) times three bags (`null`, `[]`, `'x'`). Each
draws exactly one `component-props-invalid` at the node's `properties`
path. Each walk also has two controls: a valid bag stays silent, and an
absent bag keeps the earlier verdict. Further cases: the `object-metric`
`[]` probe, an unregistered type (refused, plus a silent object-bag
control), a data-source-bound element (bag plus binding verdict, nothing
more), the top-level parse/no-duplicate pin, and pipeline gating at
`error` for `validate`, `lint` and `build`.
- The old pin "a present bag that is not an object is still left to the
parse that owns its shape" is removed. It pinned exactly the skip this
PR closes.
Gates: `node scripts/pm/dispatch-gates.mjs --commands` derived 65
families on `c444510a55`. All 65 ran, and `--ran` reconciled them with
exit codes recorded: 65 run, 0 NOT-MEASURED (a derived zero). The
`--commands` derivation on `88d3dd0572` is byte-identical. On
`88d3dd0572` I reran `check-adr-0087-registration --base origin/main`,
`check-changeset-no-major --base origin/main`, `check-empty-changeset
--base origin/main` and `check:nul-bytes`, all exit 0.
`check:dts-closure` and `check:dual-build-cjs-loads` exited non-zero in
the first sweep. That sweep ran while I was rebuilding shared dists with
`OS_SKIP_DTS=1` for the producer measurement, so the dists were
mid-rebuild. Both reran clean (exit 0) once that build finished, with no
source change between the two runs.
## Producers measured
I ran the rule over every in-repo example stack (normalized, through
`validateComponentProps`) on `c444510a55`:
| stack | pages | component-props findings | non-record bag findings |
|---|---|---|---|
| `examples/app-crm` | 1 | 0 | 0 |
| `examples/app-multi-package` | 0 | 0 | 0 |
| `examples/app-showcase` | 29 | 0 | 0 |
| `examples/app-todo` | 0 | 0 | 0 |
Positive control: I planted a nested `record:related_list` with
`properties: null` in memory into the showcase's
`showcase_component_gallery` page. It drew exactly 1 finding at
`pages[2].regions[0].components[1].properties.children[0].properties`. A
static grep of `examples/**` and the page sources for a literal
`properties: null` or `properties: []` also returns zero hits. There are
no in-repo producers to fix.
## Changeset
`.changeset/24991-nested-props-non-record.md`: `@objectstack/lint`
minor, with a BREAKING section (what moves at `os build` / `os validate`
/ `os lint` and the page save door, the remedy, what is unchanged). It
carries the claim's `Clause-②` line. The ADR-0087 disposition is
`not-required (no-migration-prescription)`: no authorable key, spelling,
export or stored shape moves. Stored pages are not converted and keep
loading until their next save, and the repair (restoring the lost props)
is not derivable by a ledger entry. `check-adr-0087-registration`
accepts it.
## Acceptance notes
- No governed surface is touched: the diff is
`packages/lint/src/validate-component-props.ts`, its test and one
changeset.
- No open PR touches `validate-component-props.ts`. I checked the files
of all 11 open PRs before opening this one.
- CI owns the repo-wide `pnpm lint` and the full battery. This PR did
not run them locally.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent baedcec commit c0ae356
3 files changed
Lines changed: 186 additions & 27 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 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
325 | 325 | | |
326 | 326 | | |
327 | 327 | | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | 328 | | |
335 | 329 | | |
336 | 330 | | |
337 | 331 | | |
338 | 332 | | |
339 | 333 | | |
340 | 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 | + | |
| 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 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
341 | 433 | | |
342 | 434 | | |
343 | 435 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
43 | 46 | | |
44 | 47 | | |
45 | 48 | | |
| |||
180 | 183 | | |
181 | 184 | | |
182 | 185 | | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
183 | 193 | | |
184 | 194 | | |
185 | 195 | | |
| |||
312 | 322 | | |
313 | 323 | | |
314 | 324 | | |
315 | | - | |
316 | | - | |
317 | | - | |
| 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 | + | |
318 | 361 | | |
319 | 362 | | |
320 | | - | |
321 | | - | |
322 | | - | |
323 | | - | |
324 | | - | |
325 | | - | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | | - | |
335 | 363 | | |
336 | 364 | | |
337 | 365 | | |
| |||
354 | 382 | | |
355 | 383 | | |
356 | 384 | | |
357 | | - | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
358 | 396 | | |
359 | 397 | | |
360 | 398 | | |
| |||
0 commit comments