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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@claudeclaudeBot 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.

beforeafterheadingtier
2020Choosing between react and htmlboth — correct as written
5454The security gatereact, already scoped inline at :68
7171What is in scopereact-only — actively misleading
102109Blocks take flat propsreact-only — actively misleading
123137Block — the escape hatch (h3)react-only — actively misleading
137156Live datareact-only — actively misleading
175200Styling … [#styling]BOTH tiers (callout) + react (remedies)
238267Accepted source shapesreact-only — actively misleading, inverted
256290When something throwsreact-only — actively misleading
266306Page statereact-only — irrelevant to html
279322record:* blocks … [#record-blocks-not-in-react]react-only — actively misleading, inverted
304354How you check your workBOTH tiers (mechanism) + react (rule names)
349402A complete pagereact-only — self-declaring
421474Relatedboth

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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

claudeBot commented Aug 31, 2026

Copy link
Copy Markdown
ContributorAuthor

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); :39no-root; :45multiple-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 — ## Stylingand## 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 ranempty. ⭐ 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 queueAug 31, 2026
Merged via the queue into main with commit d7e8f3eAug 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…d()` sites (objectstack-ai#13969)
* Delete the unreachable Array.isArray limb at all three adapter.find sites
`ObjectStackAdapter.find()` cannot resolve to an array. Re-derived on the
pinned objectui sha (9602dc82) and on objectui `origin/main`: find() has two
object-literal returns (`{ data: [], total: 0 }` for a memoized 404 and for a
fresh non-denial 404), two `normalizeQueryResult(...)` returns, and an inflight
`return existing` that hands back a promise from that same set. Both of
`normalizeQueryResult`'s branches return an object literal with exactly
`data, total, page, pageSize, hasMore` -- the first one WRAPS a bare array
response into it. So no `Array.isArray(<find result>)` limb can ever be taken.
Behaviour-preserving, like the `?? records` deletion beside it: `.data` was
already read first and always won. What goes is a shape the producer cannot
emit, in the sample a customer (and a coding agent) copies from.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
* Add the changeset for the docs sample change
`@objectstack/docs` patch, following objectstack-ai#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 objectstack-ai#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
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mtooling

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

@os-project-manager@claude