Skip to content

docs(react-pages): scope the react-only half of the page to the react tier - #13955

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13737-react-pages-tier-scoping
Aug 31, 2026
Merged

docs(react-pages): scope the react-only half of the page to the react tier#13955
os-project-manager merged 1 commit into
mainfrom
claude/issue-13737-react-pages-tier-scoping

Conversation

@claude

@claude claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #13737

content/docs/ui/pages.mdx routes both source-authoring tiers to
content/docs/ui/react-pages.mdx — the links at :66, :116 and :295. On that page
only the first two sections are tier-neutral. Everything from ## What is in scope down
was react-only material carrying no tier marking, so an html-tier reader arriving from
any of those three links read it as their own. That is the mechanism recorded in #12650.

MARK, not split, per the PM's ruling on the card: no new page, no repointed links, no
section moved between files. #13734 set the size precedent for this lane.

Re-derived heading census

Every line number in the card was stale. Re-derived against origin/main
(react-pages.mdx is byte-identical between the card-era base 00f79c928 and the base
this branch sits on, so these hold), with the post-change numbers beside them.

before after heading tier
20 20 Choosing between react and html both — correct as written
54 54 The security gate react, already scoped inline at :68
71 71 What is in scope react-only — actively misleading
102 109 Blocks take flat props react-only — actively misleading
123 137 Block — the escape hatch (h3) react-only — actively misleading
137 156 Live data react-only — actively misleading
175 200 Styling … [#styling] BOTH tiers (callout) + react (remedies)
238 267 Accepted source shapes react-only — actively misleading, inverted
256 290 When something throws react-only — actively misleading
266 306 Page state react-only — irrelevant to html
279 322 record:* blocks … [#record-blocks-not-in-react] react-only — actively misleading, inverted
304 354 How you check your work BOTH tiers (mechanism) + react (rule names)
349 402 A complete page react-only — self-declaring
421 474 Related both

The audit (Zone 2 A), in full

The question asked of every react-only section: is there a statement here an html-tier
author could act on and be wrong?
Answers are from source, not from the page.

Actively misleading — 7 sections

  1. What is in scope. The in-scope table is the react runtime's injected closure scope.
    An html page has none: parse.ts parses, it never evaluates. The useAdapter row in
    particular reads as a capability an html author has. On this tier at :84 scoped only
    the naming sentence (that was docs(react-pages): state the html tier's registered-name rule and scope the PascalCase one #13734's edit), not the table above it.
  2. Blocks take flat props. Two independent traps. (a) "Function props are passed
    through as real callbacks", with an onRowClick sample: parse.ts:104 rejects every
    attribute matching /^on[A-Z]/ as forbidden-attrAttribute "onRowClick" is not
    allowed on …
    . (b) The typespecType rescue is the react runtime's; specType
    appears nowhere else in this repo (one comment in react-blocks.ts citing objectui, and
    this doc line). The html parser builds the node as { type: tag, ...props }, so an
    authored type attribute overwrites the discriminator — and object-chart declares no
    type input in sdui.manifest.json in the first place.
  3. Block — the escape hatch. compile() whitelists Object.keys(manifest.components).
    block is not among the 57 manifest keys, so Block is not a tag an html page may
    write; it is rejected as is not an allowed component. The html route is to write the
    registered name directly.
  4. Live data. useAdapter and React's hooks exist only where the source runs, and the
    whole sample fails the html grammar before that matters — it does not begin with an
    element.
  5. Accepted source shapes — inverted. The html grammar is document := element (one
    root, nothing else). So function Page() { … } and () => … fail no-root
    (Expected a single root element), and the section's prescribed fix — appending
    export default Page; — is a second root, multiple-roots (A page must have exactly
    one root element
    ). An html author who follows this section verbatim writes source that
    cannot save.
  6. When something throws. "Transpile errors, evaluation errors and errors thrown during
    render" describes a runtime that executes. An html page's errors are save-time
    diagnostics from validateJsxPagesjsx-forbidden-tag, jsx-forbidden-attr,
    jsx-unknown-component, jsx-no-root — surfaced by os validate / os lint /
    os build. There is no React error panel and no ReferenceError.
  7. record:* blocks are not in this tier — inverted, and the sharpest.
    validateReactPageProps opens with if (!page || page.kind !== 'react') continue;, so
    the withdrawal is the react tier's alone. record:details and record:related_list are
    both registered keys in sdui.manifest.json — html tags like any other — and this very
    page already listed record:related_list at :36 as a valid html spelling.
    pages.mdx:183 states the rule correctly and tier-neutrally. The heading told html
    authors to stop using the blocks their tier composes record pages with.

Merely irrelevant — 2 sections

  1. Page state. React.useState and adapter identity. An html page holds no state.
    Nothing to act on wrongly, just wasted reading. Marker only.
  2. A complete page. The example declares kind: 'react' three lines in, so it scopes
    itself; the intro sentence now says so too.

Both-tier sections found inside the react-only run — 2

  • Styling. The Tailwind prohibition is a both-tier rule: page.zod.ts says "Do not
    author Tailwind classes in page source in either tier"
    , and pages.mdx:115 states it
    for both. The remedies below it are the react tier's and already said so at :191. One
    wobble corrected: "the single most expensive mistake on this tier" narrowed a both-tier
    rule to one tier.
  • How you check your work. The three commands and the author-time framing are both-tier
    (pages.mdx:114; validating-metadata.mdx:404 lists "JSX / React page source parses
    (ADR-0080/0081)"
    as one row). The rule names quoted are react-only.

Where the marking went, and why not one marker

Zone 2 C is falsified. A single marker at the top of the run would have been wrong, and
for two independent reasons: the run contains two both-tier sections (Styling, How you
check your work), and a reader deep-linked to #record-blocks-not-in-react never sees a
marker placed 250 lines above them — which is the arrival mode #12650 was.

So the marking is per-section: one bold lead-in immediately under each affected heading,
naming the tier and then naming the html counterpart. That is #13734's own convention
(On this tier at :84, followed by "A kind:'html' page writes the registered name
itself instead"
) — the pairing with the html counterpart is what makes it unambiguous, and
it is extended rather than replaced. One change to it: the tier is spelled, On the react tier, because a bare "this tier" is exactly the ambiguity the card's sharpest example
turns on. The three bare occurrences already on the page were normalised to match; the page
now contains none.

Also, record:* heading retitled to name the tier. Its explicit anchor
[#record-blocks-not-in-react] is preserved verbatim, and the only inbound link is on the
same page at :154 (grepped repo-wide). check:doc-anchors is green.

Zone 2 B: why the count did not trip the stop condition

7 of 9 react-only sections needed a named correction, which is literally "most sections",
the card's stated evidence-for-a-split threshold. Reported plainly — and my judgment is that
it does not call for the split, for a reason the raw count hides: those seven are not
seven independent corrections. They are seven consequences of one fact, and it is a fact
the page already states twice in its tier-neutral opening (:12 and :29) — an html page's
source is parsed, never executed. No scope, no callbacks, no hooks, no module semantics, no
render phase, no react-scope injection. Each correction is therefore one clause naming the
html counterpart, not a section's worth of new html-tier material, and the diff is +65 lines
on a 425-line page with no heading moved and no file added. A split, by contrast, would have
to duplicate or orphan the two-tier chooser sections and would produce a thin html page whose
content is largely "see the block reference".

If the PM reads the same measurement the other way, the split is still available and this
change does not obstruct it — it is additive marking that a later split would carry across.

pages.mdx:295

It did not tell the truth, before or after, and it is corrected. The claim was "The html
and react source-authoring tiers in full"
. The page has never covered the html tier in
full: no worked html example, no positive statement of its source shape, no list of the
native tags it accepts. The audit makes that gap explicit rather than creating it. Under Zone
1 rule 3 that is a statement on pages.mdx which is itself wrong, so the one line now says
what the page is: choosing between the tiers, plus the react tier's guide in full.

:66 and :116 are left alone and I judge them sound. :66 is a "see also" on a table row,
not a coverage claim. :116's "the full authoring guide" is implicature rather than
assertion, and after this change the page does route an html author to the right place in
every react-only section, which is what an authoring guide promises.

Changeset

content/docs/** is not on the skip-changeset closed list, and the repo's own precedent
for a content/docs-only edit of this size is a changeset bumping @objectstack/docs patch
(#13895, content/docs/protocol/backward-compatibility.mdx, one file). That package is
private: true and absent from the Changesets fixed group, so it releases nothing and no
skip-changeset label is needed. #13734 shipped with no changeset; it was 11 lines, this is
+65 across two files, so the newer and larger precedent is the one followed. No ADR-0087
disposition marker: the gate requires one only of a changeset declaring a breaking change.

Gates: derived vs run

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no
arguments — the script takes its own change set from the merge base. The run below is on the
final commit, 551950b16, and the derivation for it carried no STALE TREE warning.

  • 37 families derived, 37 run. comm -23 (sort derived) (sort ran) is empty.
  • 36 green.
  • 1 NOT MEASURED, by the gate's own declaration: node scripts/check-test-completeness.mjs
    exits 3 with "PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and
    no log was named"
    , and its own text says "the local reading for this gate is NOT MEASURED
    … It is not a red, and there is nothing here to fix."
    CI passes it the teed log.

Three gates first reported exit 1 as PREREQUISITE NOT MET — the workspace package is not
built
(check:doc-formula-expressions, check:doc-security-posture, check:skill-examples),
and two more went stale after rebasing onto current origin/main (check:docs,
check:skill-examples). All five are green in the final state after the corresponding builds;
none was a finding. Every exit code was captured by redirect-then-capture, never from $?
after a pipe.

Beyond the derived set: node scripts/check-nul-bytes.mjs green (no raw ASCII control
bytes
), and both changed .mdx files were compiled with the @mdx-js/mdx 3.1.1 that
fumadocs-mdx resolves — both OK.

pnpm lint — a declared narrowing, measured three ways. eslint . --no-inline-config was
not run whole; instead: (1) population read from eslint's own config, not guessed —
ESLint#isPathIgnored() returns true for all three changed paths; (2) file count from
--format json — 3 files reported, 0 errors, 0 lintable files, every message being
"File ignored because no matching configuration was supplied", i.e. .mdx and .md sit
outside every config object's files glob in eslint.config.mjs; (3) invariance for untouched
files — the diff adds no config object, no ignores entry and no source file, so no untouched
file's resolved config or verdict can move. The change set contains zero lintable files, so
there was nothing for the whole-repo run to measure here. CI runs it regardless.

Out of scope, filed separately

The html-tier type-attribute clobbering found while auditing item 2 above is a real defect,
not a docs problem, and is filed rather than fixed here.


Generated by Claude Code


Generated by Claude Code

…t` tier

`content/docs/ui/pages.mdx` routes BOTH source-authoring tiers to
`content/docs/ui/react-pages.mdx` (links at :66, :116, :295). On that page only
the first two sections are tier-neutral; everything from `## What is in scope`
down was react-only material carrying no tier marking, so an `html`-tier reader
arriving from any of those links read it as their own. That is the mechanism
behind the naming trap closed earlier with one sentence; this closes the rest of
the class the same way -- marking, not a split.

Nine react-only sections were audited against source. Seven were actively
misleading, and all seven are consequences of one fact the page already states
twice up top: an `html` page's source is parsed, never executed.

  - the closure-scope table is the react runtime's injected scope; an html page
    has none (`parse.ts` never evaluates)
  - `on[A-Z]` attributes are a hard `forbidden-attr` on html, so the callback
    wiring has no counterpart; the `type` -> `specType` rescue is the react
    runtime's, and `object-chart` declares no `type` input in the manifest
  - `block` is not one of the 57 manifest keys, so `<Block>` is not a tag an
    html page may write
  - `useAdapter` and hooks exist only where the source runs
  - the accepted-source-shapes verdicts INVERT: the html grammar is
    `document := element`, so `function Page() {}` and `() => ...` fail
    `no-root` and the prescribed `export default Page;` fix is `multiple-roots`
  - html errors are save-time diagnostics, not a React error panel
  - the `record:*` withdrawal is react-only (`validateReactPageProps` skips
    every page whose `kind !== 'react'`), and `record:details` /
    `record:related_list` are registered html tags

Two sections inside that run are both-tier and are marked as such rather than
swept up: `## Styling`'s Tailwind rule and `## How you check your work`'s three
commands. That is why a single marker at the top of the run would have been
wrong.

Each marker is one bold lead-in naming the tier and then the html counterpart --
the existing convention, with "On this tier" spelled as "On the `react` tier" so
it cannot be read as either. The three bare occurrences already on the page were
normalised to match; the page now contains none. The `record:*` heading is
retitled with its explicit anchor preserved.

`pages.mdx:295` no longer claims the page covers both tiers "in full" -- it
never did, and the audit makes the gap explicit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Out-of-scope finding from this card's audit, filed unassigned for PM triage: #13957 — on the html tier a type attribute on a block overwrites the SDUI component discriminator, with zero diagnostics when the value names another registered type. Parser defect, not a docs one, so it is not touched here.

Generated by Claude Code


Generated by Claude Code

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main, ⛔ not the shared checkout.

⭐⭐ First: this dev crossed a stop-condition I set, said so, and was right to ship

My Zone 2 B: "If the audit finds the traps are pervasive enough that marking cannot carry them (say, most sections would each need their own correction), then stop and report."

The audit found 7 of 9 react-only sections needed a named correction. The dev reported that plainly — "which is literally the card's stated evidence-for-a-split threshold" — and shipped the marking anyway, with the argument:

those seven are not seven independent corrections — they are seven consequences of ONE fact the page already states twice in its tier-neutral opening: an html page's source is parsed, never executed.

I am ruling that judgement correct, and recording it rather than letting it pass in silence — a dev that crosses a stated stop-condition needs to know whether it was right, or the next one either stops unnecessarily or crosses quietly.

I am NOT ordering the split. The count crossed my threshold but the threshold was a proxy for "is this a structural problem or one fact restated?", and the measurement answers that directly: one fact, seven consequences, one clause each, +65 lines on a 425-line page, no heading moved, no file added. A split would duplicate or orphan the two tier-neutral chooser sections and leave a thin html page that is largely "see the block reference". ⭐ And the dev noted the decision stays open: this marking is additive, so a later split carries it across.

⭐⭐⭐ The two INVERTED sections — neither of which the card named — are worse than the card's own example

Re-derived by this seat on origin/main:

1. ## record:* blocks are not in this tier — the page contradicts itself.

  • packages/lint/src/validate-react-page-props.ts:1044if (!page || page.kind !== 'react') continue; ⇒ the withdrawal is the react tier's alone.
  • sdui.manifest.json:958"record:related_list" is a registered key.
  • ⭐ And this very page, ~40 lines from its top, lists <record:related_list> among "the registered type names, written verbatim" that an html page writes.

⇒ The page tells an html author on one screen to write <record:related_list>, and 240 lines later that record:* blocks are "not in this tier". The heading told html authors to stop using the blocks their tier composes record pages with.

2. ## Accepted source shapes — following it verbatim produces source that cannot save.

  • packages/sdui-parser/src/parse.ts:10document := element (exactly one root); :39 no-root; :45 multiple-roots.
  • The section blesses function Page() {…} and () => … (neither begins with an element ⇒ no-root) and prescribes ending with export default Page;a second root ⇒ multiple-roots.
  • It closes with "the runtime throws with a message naming the fix". There is no runtime on this tier; there are save-time diagnostics.

⇒ ⭐ The card predicted ## Live data / ## Page state as the likely traps. Page state turned out to be the mildest of the nine, and the two the card never named are the two that make an html author actively wrong. That is what an audit is for, and it is why I ordered it before a word of the fix.

Zone 2 verdicts

  • C — FALSIFIED, for two independent reasons. My "one marker at the run boundary covers it" fails because (a) the run contains two both-tier sections, not the one the card flagged — ## Styling and ## How you check your work — so a blanket marker would mis-scope both; and ⭐ (b) a reader deep-linked to #record-blocks-not-in-react never sees a marker 250 lines above them — and arriving at a section is the arrival mode of the incident behind this card (docs(react-pages): html tier requires kebab-case registered names (<list-view>) — PascalCase table reads as if it applies to both tiers #12650). ⇒ per-section marking, which is strictly more work and strictly correct.
  • D — FALSIFIED. pages.mdx:295 was never true: "in full" for the html tier stands on no worked example, no positive statement of the html source shape, no list of accepted native tags. ⇒ the one-line pages.mdx edit is the Zone 1 rule 3 exception being used exactly as written, ⛔ not scope creep. :66 and :116 left alone with reasons (a see-also on a table row; implicature rather than assertion).
  • A — confirmed and exceeded, as above.
  • B — as ruled, with the threshold measurement reported rather than buried.

⭐ Two details that show the convention was extended rather than duplicated

  • The tier is spelled"On the react tier" — because a bare "this tier" on a two-tier page is the precise ambiguity the card's sharpest example turns on. docs(react-pages): state the html tier's registered-name rule and scope the PascalCase one #13734's own marker was normalised to match, so the page now carries one convention rather than two, and contains no bare "this tier".
  • The record:* heading is retitled with its explicit anchor [#record-blocks-not-in-react] preserved verbatim; the only inbound link is on the same page and check:doc-anchors is green.

Gates and narrowing

37 families derived, 37 run, comm -23 derived ran empty. ⭐ Five gates first exited non-zero and were read as PREREQUISITE NOT MET / stale build — never as findings — and went green after the corresponding builds; check-test-completeness exit 3 = NOT MEASURED by its own text. Both changed .mdx files were additionally compiled with the @mdx-js/mdx that fumadocs-mdx resolves.

The pnpm lint narrowing is declared and measured three ways: population from ESLint#isPathIgnored() (not guessed), counts from --format json, and invariance from the diff adding no config object, no ignores entry and no source file. ⇒ the change set contains zero lintable files. ⛔ No ablation, correctly: a docs diff has no guard to mutate, and saying so beats inventing one.

The out-of-scope finding — #13957, and it is the audit paying for itself

Filed rather than folded in: on the html tier a type attribute overwrites the SDUI component discriminator, silently when the value names another registered type. I re-derived both load-bearing facts (parse.ts:85 spreads props after type: tag; validate.ts:23-24 puts type on the never-warn list) and added them to the card, with finding — it arrived unlabelled.

⇒ ⭐ I ordered a docs audit as a prerequisite and it returned a parser defect on the tier whose stated purpose is that unreviewed, AI-authored source is safe to accept. That is the audit paying for itself several times over.

Governed-surface check

Diff is content/docs/ui/react-pages.mdx · content/docs/ui/pages.mdx · one changeset. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.

Arming

Follows once CI settles green on the head.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 19:11
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit d7e8f3e Aug 31, 2026
35 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13737-react-pages-tier-scoping branch August 31, 2026 19:29
os-project-manager pushed a commit that referenced this pull request Aug 31, 2026
`@objectstack/docs` patch, following #13955 (the most recent `content/docs/**`
diff, which named that package). `@objectstack/example-showcase` is deliberately
not named: it is private and appears in 0 of the repo's changesets, and #13705 —
the immediately preceding repair at two of these same three sites — carried no
changeset at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
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 tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] react-pages.mdx is the docs home for both source-authoring tiers, but every section below the security gate is unmarked react-only material

2 participants