Skip to content

feat(tooling): sweep the react-page useAdapter() contracts over the docs corpus too - #11584

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-10751-react-page-guard-docs-corpus
Aug 24, 2026
Merged

feat(tooling): sweep the react-page useAdapter() contracts over the docs corpus too#11584
os-steve merged 1 commit into
mainfrom
claude/issue-10751-react-page-guard-docs-corpus

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#10751

One wrong read — result.records off an ObjectStackAdapter.find() result, where the
normalized QueryResult only ever declares data — was repaired three separate times.
The guard left behind after the second repair had the right detectors and the wrong
population: it read the app-showcase page registry, so the react samples in
content/docs were invisible to it, which is how instance 3 survived the two code fixes
and stayed live in the copy a customer starts from.

recordsOnlyReads() and unprefixedQueryKeys()move into
scripts/check-react-page-adapter-contract.mjs (pnpm check:react-page-adapter-contract),
which sweeps both populations. They move rather than being copied: two definitions of one
detector double the places a future fix has to land, which is the defect this card is
about, not a fix for it. examples/app-showcase/test/react-page-adapter-query-contract.test.ts
keeps the half a text scan cannot do — it executes the renewals-pipeline rollup against a
contract-faithful adapter double (5 tests, still green).

Route taken, and the measurement that decided it

A gate in the docs family, not the example app's test with a wider reach.

check:cross-package-test-inputs (re-read on today's main, post-#11486/#11554) requires
a declaration plusturbo.json hashing every declared glob. Both available spellings
are wrong here:

spellingmeasurementverdict
content/docs/**22 of the last 132 commits on main touch it (16.7%), against 3 that touch examples/app-showcase. 19 of those 22 carry no react sample at all.Puts the example app's whole suite on ~1 PR in 6 — the exact cost that gate's own roster refuses twice, in those words: "Per-page rather than content/docs/**: docs are edited far more often than any package here."
per-page (content/docs/ui/react-pages.mdx)adapter.find( occurs in exactly one docs page today.Green and complete today; silently incomplete the day a react sample lands on a second page. A list someone must remember to extend is this defect.

A scripts/check-*.mjs gate has no radius to maintain — lint.yml runs it on every PR over
the whole tree, so a new docs page is in the population the moment it exists. Confirmed
empirically: check:cross-package-test-inputs is green at 9e9415ff with no new
declaration (14 package(s) read outside themselves, all declared).

The population, measured — what it admits and what it misses

21 app-showcase page module(s) + 1 content/docs react-page sample(s) (from 394 doc file(s), 1951 fenced block(s)).

1 of 1951 fences..find( alone is not the marker, and that is the whole difficulty —
this tree has three different find() contracts in its docs and only one declares data:

  • adapter.find / dataSource.find — objectui ObjectStackAdapter, $-prefixed options,
    rows under data. Swept.
  • engine.find / dataEngine.find — ObjectQL IDataEngine, whose options are where /
    fields / sort, unprefixed by contract. content/docs/protocol/objectql/query-syntax.mdx
    alone holds 11 of them; sweeping those fabricates ~30 findings.
  • client.data.find / useQuery@objectstack/client, which resolves a PaginatedResult
    whose rows are under records (packages/client/src/index.ts:310).
    content/docs/api/client-sdk.mdx:659 reads data?.records.map(...) and is correct.
    Sweeping it fabricates a fourth instance of a defect that is not there.

So the marker is the identifier holding the adapter, not the method name: useAdapter(, or
a find/findOne on adapter / dataSource. A language-tagged fence qualifies on either;
an untagged fence needs the real call (which keeps react-pages.mdx's untagged CLI-error
block, whose hint names useAdapter().findOne in prose, out while still catching a sample
someone forgot to tag); any other tag (bash, json, text) is taken at its word.

Stated exclusions, rather than discovered later: a deliberate counter-example written as a
runnable fence would be flagged (this doc set writes counter-examples as prose — see the
"$ prefixes are load-bearing" Callout — and no fence in the tree is one today, so no opt-out
is invented); a sample holding the adapter under a third name with no useAdapter( in the
fence; content/ outside docs/, and the docs/ ADR tree.

The census control, re-pointed

The old control guarded a vacuously-green empty sweep of app-showcase only. Once the
population includes content/docs, that no longer covers the new half. censusFailures()
now fails when either half comes back empty, and additionally pins each of the three
files the defect was actually repaired in as an anchor — so a population that is non-empty
but has lost coverage fails too. Non-empty and covering are different facts.

Verification

Everything below at 9e9415ff.

Non-vacuity, both directions, under trap restore EXIT INT TERM, each mutation proven
on disk by git hash-object and by counting the injected and removed markers:

legmutationhash movedgate
docs corpus$top:top:, result?.data ?? …result.records in the docs fenceb4ec8fb3eb0bcf88exit 1, 2 findings, both at content/docs/ui/react-pages.mdx
app-showcase$top: 500top: 500 (2 sites) in renewals-pipeline.page.ts2814e1668bfe5e4bexit 1, 2 findings, separately located
censusthe docs sample renamed so it stops matching the selectorb4ec8fb3ea203fc7exit 1, contributed 0 react-page sample(s) + the missing anchor

Every leg restored byte-identical (b4ec8fb3 / 2814e166 recovered exactly), and the
gate returned to exit 0 after each. The mutation step never trusts an editor's exit code:
each leg aborts on an anchor miss and prints injected/removed marker counts.

The existing positive control still fires — carried verbatim into the gate's
--self-test, which is a strict promotion (it now runs on every PR rather than only when
app-showcase is affected):

✓ check-react-page-adapter-contract --self-test: 30 assertions — both detectors observed
FIRING and observed silent, the selector observed refusing the engine.find / useQuery /
webhook shapes it must not fabricate on, and an empty sweep of EITHER half observed
failing the census.

Derived gate unionnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
(no hand-written path list; the script takes the change set from the merge base itself)
named 22 path-matched families plus the convention-triggered set for a test-file edit.
All 24 run green, exit codes captured before any pipe. Verdict lines rather than bare $?:

check-react-page-adapter-contract: 21 app-showcase page module(s) + 1 content/docs
react-page sample(s) (from 394 doc file(s), 1951 fenced block(s)) — every adapter query
option is $-prefixed and every row read is off `data`.
check-cross-package-test-inputs: 14 package(s) read outside themselves, all declared, and
turbo.json hashes every declared glob.
check-entry-guard: 140 scripts/ file(s) — every entry guard goes through invoked-as.mjs
check-test-source-alias OK — 72 packages with tests scanned

Package scope: pnpm --filter '@objectstack/example-showcase^...' build (dependency
closure first, on a fresh worktree), then the app-showcase suite — Test Files 25 passed (25), Tests 359 passed (359) — and pnpm --filter @objectstack/example-showcase typecheck
exit 0. The touched test file alone: Test Files 1 passed (1), Tests 5 passed (5).

Lint, narrowed with the narrowing proven rather than skipped. eslint over exactly the
four changed paths: 4 accepted, 0 errors, 2 warnings — both of them eslint's own
File ignored because no matching configuration was supplied for package.json and
lint.yml, so the checkable population is read from eslint's config, not guessed. Counts
from --format json. Invariance: eslint.config.mjs enables type-aware linting for no
file (0 matches for projectService / parserOptions.project; the config's own header
records the measurement), so this diff cannot move any untouched file's verdict.

Not measured locally, declared: check:type-check-debt --re-measure, which needs the
full workspace closure built. CI owns it. The diff removes test code and adds a
dependency-free script, and @objectstack/example-showcase's own typecheck is green.

Changeset

None — skip-changeset. Every touched package publishes nothing: examples/app-showcase
is "private": true, the root package.json is private, and scripts/ and
.github/workflows/ are not published at all. That is scripts/pr-labels.mjs's own rule:
"skip-changeset is the exemption for a PR that publishes nothing."

Premise re-check (the card is from 08-21)

Re-swept before changing anything. .records off an adapter result in content/docs
today: one occurrence, and it is not a fourth instance — content/docs/ui/react-pages.mdx:147
reads result?.data ?? result?.records ?? (Array.isArray(result) ? result : []), where
.data is read first and always wins, so the sample renders correctly. Instance 3 is still
fixed. The other .records hits in the corpus are the data.records.updated event-type
string and the client-sdk.mdxPaginatedResult read, which is correct on its own contract.

That surviving ?? alias is a real contract-first smell, and the detector's
.data-beside carve-out is what blesses it — but the same shape is what instance 1's
landed fix uses (crm-workbench.page.ts:49), so tightening the detector would redden a
different file and a different instance. Filed separately rather than folded in silently.


Generated by Claude Code

…ocs corpus too
One wrong read -- `result.records` off an `ObjectStackAdapter.find()` result,
where the normalized `QueryResult` only ever declares `data` -- was repaired
three separate times: two app-showcase pages and `content/docs/ui/react-pages.mdx`.
The guard left behind after the second one had the right detectors and the wrong
population: it read the app-showcase page registry, so the react samples in
`content/docs` -- the copy a customer starts from -- were invisible to it.
`recordsOnlyReads()` and `unprefixedQueryKeys()` MOVE into
`scripts/check-react-page-adapter-contract.mjs`, which sweeps both populations.
They move rather than being copied: two definitions of one detector double the
places a future fix has to land, which is the defect, not a fix for it. The
example app's test keeps the half a text scan cannot do -- it executes the
renewals-pipeline rollup against a contract-faithful adapter double.
A gate rather than that test with a wider reach, decided by measurement:
`content/docs/**` as a declared cross-package test input would put the example
app's suite on 22 of the last 132 commits (against 3 that touch the app), which
is the cost `check-cross-package-test-inputs`'s own roster refuses twice; and the
per-page narrowing it prefers instead rebuilds this defect, since a list someone
must remember to extend goes silently incomplete the day a react sample lands on
a second page.
The census control is re-pointed: an empty sweep of EITHER half fails, and each
of the three files the defect was repaired in is pinned as an anchor, so a
population that is non-empty but has lost coverage fails too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actionsgithub-actionsBot added size/l ci/cd dependencies Pull requests that update a dependency file tests labels Aug 24, 2026
@os-steve
os-steve marked this pull request as ready for review August 24, 2026 07:47
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit eb10fcfAug 24, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-10751-react-page-guard-docs-corpus branch August 24, 2026 08:06
os-steve pushed a commit that referenced this pull request Aug 24, 2026
#11542)
Eight scans in `affected-docs.mjs` ask "is a `route:` / `client:` declaration
written here?". `declLead` has spelled the colon and the run after it once since
#11494, but the KEY stayed each call site's own argument: `declarationsIn`
anchored it with `\b` and the other seven did not. So `subroute: 'GET /x'` was a
declaration to SEVEN of the eight and not to the eighth, and it minted a silent
phantom ROW — silent because the partial-read verdict keys on the gap between
`rows` and `routesDeclared` and both terms read the unanchored spelling.
The anchor now lives in `declLead`, once, and `declarationsIn` stops restating
it. Priced against the `--bridge-coverage` before/after standard: 0 divergent
leads across the seven live ledgers, and the census is byte-identical row for
row (269 of 269 / 222 of 222 / 45 reachable / 177 UNREACHABLE / 0 prose-quoted
leads / brokenScan 0).
`--self-test` moves the pin #11584 left rather than deleting it, adds one
fixture per previously-unanchored scan, and pins the `$route:` residue (#11630)
where the next card will find it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] the #10288 react-page adapter-contract guard sweeps only app-showcase pages, so the same .records read survived a third time in content/docs

2 participants

@os-steve@claude