Skip to content

fix(lint): refuse a react-page ListView bound only by the canonical data source - #15025

Merged
zhuangjianguo merged 3 commits into
mainfrom
claude/issue-14791-listview-lint-stop-blessing-canonical
Sep 3, 2026
Merged

zhuangjianguo merged 3 commits into
mainfrom
claude/issue-14791-listview-lint-stop-blessing-canonical

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Part of #14791

Step 1 of 3 of the maintainer's ruling B (recorded on the card, 2026-09-03, verbatim 「同意」). This is the spec lane's step 1 only — the validator stops blessing a react-page ListView bound only by the canonical spelling. The card stays open: step 2 (objectui's consumer fold) and step 3 (retire the aliases, re-enable canonical binding, rename the sites) remain. objectui#7477 is step 2. #14343 remains blocked until step 3.

⛔ No site renames, no packages/spec change, nothing in objectui — all three are later steps.

The defect

The react-blocks contract deprecates ListView's objectName in favour of data={{ provider: 'object', object }}, and this rule treated a present deprecated.replacedBy as satisfying the required binding. No renderer reads that spelling, so a page written to the contract's own summary passed os validate and then rendered an empty list with no diagnostic. The rule's own comment justified its canonical-first precedence as "mirroring the one-directional fold objectui's normalizeListViewSchema applies at the component boundary" — that fold does not exist.

Re-measured on the objectui tip a27d153c, newer than the readings on the card, in packages/plugin-list/src/ListView.tsx:

spelling occurrences
data.object 0
dataConfig.object 0
provider === 'object' 0
specType 0
schema.objectName 52

packages/core/src/utils/normalize-list-view.ts contains zero .object reads. The premise holds.

What changed — packages/lint/src/validate-react-page-props.ts

  • The replaced-by satisfaction no longer applies to ListView's object binding when the author wrote the object provider (objectProviderBindsNothing, a named exception citing the ruling). The generic mechanism the contract publishes is kept for every other block.
  • boundObjectName drops its canonical-first branch. Field-name props resolve against objectName again — which is also the correct object when a page carries both spellings, since that is the one the renderer queries.
  • The false fold comment is gone. What replaces it records, dated, that the fold does not exist, so the assumption cannot be reintroduced by the next reader.
  • The missing-required diagnostic no longer offers data={…} (canonical) for ListView; it names objectName, and adds one clause explaining that the canonical source would render an empty list. The deprecation warning keeps its severity and now says objectName / viewType are still the spellings to write.

One measured refinement to the dispatch

The dispatch framed the tightening as ListView losing the replaced-by satisfaction. Measurement narrowed it to the object provider only. ListView.tsx's fetch effect implements provider: 'value', a plain-array shorthand and the renderer-owned api provider; only provider: 'object' falls through all three to the objectName-gated fetch, which clears the loading state and returns no rows. Refusing the providers that do render would tighten past what renders, against this step's own stated reason (Clause-②: no). A non-static data still satisfies — unresolvable is not wrong, ADR-0072 D1. All three cases are pinned.

Blast radius measured, not assumed: ListView is the only block in react-blocks.ts declaring any deprecated prop, so a ListView-scoped exception and a general change are behaviourally identical today. The scoped one was chosen because deleting the mechanism outright would falsify the contract's own doc comment in packages/spec, which step 3 owns and this PR may not touch.

Tests

The vocabulary-window cases that pinned the canonical spelling as binding are inverted, not deleted — the window is still open, only its blessing is withdrawn. Two new pins: a canonical-only page is refused with a diagnostic naming objectName; an objectName page passes. Three more pin the scope (value provider, non-static data, and the deprecated spelling winning when both are present).

Every other ListView fixture merely used the canonical spelling as a convenient binding; each is re-spelled to objectName, without which those suites would resolve fields against nothing and pass vacuously. That adds one deprecation warning per fixture, so the field-resolution suites read past that single rule.

No real site goes red. All five checked-in ListView sites bind with objectName: skills/objectstack-ui/rules/pages.md:180 and :218, examples/app-showcase/src/ui/pages/crm-workbench.page.ts:88, renewals-pipeline.page.ts:136 and :169, task-desk.page.ts:44. Zero sites anywhere write the canonical spelling. The other provider: 'object' hits in the repo are metadata-tier list views, a different schema this rule never reads.

Verification — all at the final commit 3120681a

  • pnpm --filter @objectstack/lint test — 93 files, 2873 passed, 5 skipped.
  • pnpm --filter @objectstack/lint typecheck — clean, including the shrink-only test-layer ratchet (check:test-typecheck: OK).
  • Gate family re-derived on the actual diff with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack after the merge, from a tree with no staleness warning: 36 of 39 pass. The other three exit 3, their own "PREREQUISITE NOT MET" code, which each states is neither a pass nor a finding: check-test-completeness needs a saved turbo test log CI tees and this branch cannot produce, and check:dual-build-cjs-loads / check:type-check-debt need every package's dist. Recorded as NOT MEASURED; CI measures them.
  • Reverse verification, fix committed first, direction predicted before running: restoring only the source to the pre-fix base should turn the new pins red. Mutation confirmed on disk by marker counts (guard 3 to 0, removed comment 0 to 1) and a blob hash differing from HEAD; result 6 failed / 157 passed, exactly the six predicted cases, the 22 re-spelled fixtures staying green as predicted. Restore proven by observed state, not an exit code: git diff HEAD empty, git status --porcelain empty (no poisoned index), and the file byte-identical to the HEAD blob e94f2370. No build leg is involved — the suite imports the rule by relative path, so vitest reads the mutated source directly.
  • Declared narrowing: the repo-wide pnpm lint was not run; eslint --no-inline-config --format json ran on the two changed files, 0 errors and 0 warnings, file count 2 read from the JSON output. This cannot have moved any untouched file's verdict: eslint.config.mjs:327-329 states this repo "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file".

🤖 Generated with Claude Code

Generated by Claude Code


Generated by Claude Code

…source

Step 1 of the maintainer's three-step ruling. The react-blocks contract
deprecates ListView's `objectName` in favour of
`data={{ provider: 'object', object }}`, and this rule treated the canonical
replacement as satisfying the required binding. No renderer reads it: objectui's
ListView queries `dataSource.find(schema.objectName, ...)` and its
`normalizeListViewSchema` has no `.object` read at all, so a page written to the
contract's own summary validated green and rendered an empty list.

- The replaced-by satisfaction no longer applies to ListView's object binding
  when the author wrote the object provider. Scoped to that provider on
  purpose: the fetch effect does implement `provider: 'value'`, a plain-array
  shorthand and the renderer-owned `api` provider, so refusing those would
  tighten past what renders.
- `boundObjectName` drops its canonical-first branch — field refs resolve
  against the spelling the renderer reads, which is also the right one when an
  author writes both.
- The comment claiming the precedence mirrored a one-directional fold in
  objectui's `normalizeListViewSchema` is gone; that fold does not exist.
- The missing-required diagnostic and the deprecation warning now point at
  `objectName` as the spelling that renders today.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…nders

The four cases that pinned the canonical metadata-tier spelling as binding a
ListView are turned round rather than deleted — the deprecation window is still
open, only its blessing is withdrawn. Two new pins: a canonical-only page is
refused with a diagnostic naming `objectName`, and an `objectName` page passes.
Three cases pin the scope: the `value` provider and a non-static `data` still
satisfy (both render), and the deprecated spelling now wins when a page carries
both, because that is the one objectui queries.

Every other ListView fixture merely USED the canonical spelling as a convenient
binding; each is re-spelled to `objectName`, which is what those suites need to
keep resolving fields against a real object rather than against nothing. That
adds one deprecation warning per fixture, so the suites about field resolution
read past that single rule via `pastDeprecation`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
@github-actions github-actions Bot added the size/m label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 8 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/objectui/concept.mdx (via viewType (literal, a string literal in UNFOLDED_DEPRECATIONS))
  • content/docs/ui/react-pages.mdx (via viewType (literal, a string literal in UNFOLDED_DEPRECATIONS))
What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (literal, 34 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fddfc8db062d61ca68ba482531f5368326109554 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9141529d2ee8fdb7537b95febaed362c15256e10 — the merge of head 3120681a7a489cd04825d7bd470b38442bbe4bf7 into base fddfc8db062d61ca68ba482531f5368326109554, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9141529d2ee8fdb7537b95febaed362c15256e10 && git checkout 9141529d2ee8fdb7537b95febaed362c15256e10
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fddfc8db062d61ca68ba482531f5368326109554 3120681a7a489cd04825d7bd470b38442bbe4bf7 && git checkout -B drift-repro fddfc8db062d61ca68ba482531f5368326109554 && git merge --no-ff 3120681a7a489cd04825d7bd470b38442bbe4bf7

node scripts/docs-audit/affected-docs.mjs --json fddfc8db062d61ca68ba482531f5368326109554

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fddfc8db062d61ca68ba482531f5368326109554 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator

Landing provenance (PM seat domain:spec, session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T18:10Z) — flipping to ready and enabling auto-merge (squash).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants