docs(react-pages): scope the react-only half of the page to the react tier - #13955
Conversation
…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
|
Out-of-scope finding from this card's audit, filed unassigned for PM triage: #13957 — on the html tier a Generated by Claude Code Generated by Claude Code |
|
ACCEPT — ⭐⭐ First: this dev crossed a stop-condition I set, said so, and was right to shipMy 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:
⇒ 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 exampleRe-derived by this seat on 1.
⇒ The page tells an html author on one screen to write 2.
⇒ ⭐ The card predicted Zone 2 verdicts
⭐ Two details that show the convention was extended rather than duplicated
Gates and narrowing37 families derived, 37 run, The The out-of-scope finding — #13957, and it is the audit paying for itselfFiled rather than folded in: on the html tier a ⇒ ⭐ 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 checkDiff is ArmingFollows once CI settles green on the head. Generated by Claude Code |
`@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
Fixes #13737
content/docs/ui/pages.mdxroutes both source-authoring tiers tocontent/docs/ui/react-pages.mdx— the links at:66,:116and:295. On that pageonly the first two sections are tier-neutral. Everything from
## What is in scopedownwas react-only material carrying no tier marking, so an
html-tier reader arriving fromany 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.mdxis byte-identical between the card-era base00f79c928and the basethis branch sits on, so these hold), with the post-change numbers beside them.
reactandhtml:68Block— the escape hatch (h3)record:*blocks … [#record-blocks-not-in-react]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
An html page has none:
parse.tsparses, it never evaluates. TheuseAdapterrow inparticular reads as a capability an html author has.
On this tierat:84scoped onlythe 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.
through as real callbacks", with an
onRowClicksample:parse.ts:104rejects everyattribute matching
/^on[A-Z]/asforbidden-attr— Attribute "onRowClick" is notallowed on …. (b) The
type→specTyperescue is the react runtime's;specTypeappears nowhere else in this repo (one comment in
react-blocks.tsciting objectui, andthis doc line). The html parser builds the node as
{ type: tag, ...props }, so anauthored
typeattribute overwrites the discriminator — andobject-chartdeclares notypeinput insdui.manifest.jsonin the first place.Block— the escape hatch.compile()whitelistsObject.keys(manifest.components).blockis not among the 57 manifest keys, soBlockis not a tag an html page maywrite; it is rejected as is not an allowed component. The html route is to write the
registered name directly.
useAdapterand React's hooks exist only where the source runs, and thewhole sample fails the html grammar before that matters — it does not begin with an
element.
document := element(oneroot, nothing else). So
function Page() { … }and() => …failno-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 exactlyone root element). An html author who follows this section verbatim writes source that
cannot save.
render" describes a runtime that executes. An html page's errors are save-time
diagnostics from
validateJsxPages—jsx-forbidden-tag,jsx-forbidden-attr,jsx-unknown-component,jsx-no-root— surfaced byos validate/os lint/os build. There is no React error panel and noReferenceError.record:*blocks are not in this tier — inverted, and the sharpest.validateReactPagePropsopens withif (!page || page.kind !== 'react') continue;, sothe withdrawal is the react tier's alone.
record:detailsandrecord:related_listareboth registered keys in
sdui.manifest.json— html tags like any other — and this verypage already listed
record:related_listat:36as a valid html spelling.pages.mdx:183states the rule correctly and tier-neutrally. The heading told htmlauthors to stop using the blocks their tier composes record pages with.
Merely irrelevant — 2 sections
React.useStateand adapter identity. An html page holds no state.Nothing to act on wrongly, just wasted reading. Marker only.
kind: 'react'three lines in, so it scopesitself; the intro sentence now says so too.
Both-tier sections found inside the react-only run — 2
page.zod.tssays "Do notauthor Tailwind classes in page source in either tier", and
pages.mdx:115states itfor both. The remedies below it are the react tier's and already said so at
:191. Onewobble corrected: "the single most expensive mistake on this tier" narrowed a both-tier
rule to one tier.
(
pages.mdx:114;validating-metadata.mdx:404lists "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-reactnever sees amarker 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 tierat:84, followed by "Akind:'html'page writes the registered nameitself 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 exampleturns 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 thesame page at
:154(grepped repo-wide).check:doc-anchorsis 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 (
:12and:29) — an html page'ssource 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:295It did not tell the truth, before or after, and it is corrected. The claim was "The
htmland
reactsource-authoring tiers in full". The page has never covered the html tier infull: 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.mdxwhich is itself wrong, so the one line now sayswhat the page is: choosing between the tiers, plus the
reacttier's guide in full.:66and:116are left alone and I judge them sound.:66is a "see also" on a table row,not a coverage claim.
:116's "the full authoring guide" is implicature rather thanassertion, 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 theskip-changesetclosed list, and the repo's own precedentfor a
content/docs-only edit of this size is a changeset bumping@objectstack/docspatch(#13895,
content/docs/protocol/backward-compatibility.mdx, one file). That package isprivate: trueand absent from the Changesetsfixedgroup, so it releases nothing and noskip-changesetlabel 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, noarguments — 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.comm -23 (sort derived) (sort ran)is empty.node scripts/check-test-completeness.mjsexits 3 with "PREREQUISITE NOT MET — this gate grades a saved
turbo run testlog, andno 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.mjsgreen (no raw ASCII controlbytes), and both changed
.mdxfiles were compiled with the@mdx-js/mdx3.1.1 thatfumadocs-mdxresolves — both OK.pnpm lint— a declared narrowing, measured three ways.eslint . --no-inline-configwasnot run whole; instead: (1) population read from eslint's own config, not guessed —
ESLint#isPathIgnored()returnstruefor 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.
.mdxand.mdsitoutside every config object's
filesglob ineslint.config.mjs; (3) invariance for untouchedfiles — the diff adds no config object, no
ignoresentry and no source file, so no untouchedfile'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