Skip to content

fix(spec): give the client-SDK surface a page-scoped docs root so SDK pages can opt into check:skill-examples - #12339

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-12048-sdk-docs-surface-resolution
Aug 25, 2026
Merged

fix(spec): give the client-SDK surface a page-scoped docs root so SDK pages can opt into check:skill-examples#12339
os-litant merged 2 commits into
mainfrom
claude/issue-12048-sdk-docs-surface-resolution

Conversation

@os-litant

@os-litantos-litant commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12048

What was wrong

check:skill-examples resolves modules per surface, and a SourceRoot was always a whole subtree — so "which packages may this page import?" was a property of the tree a page happens to live in. content/docs/** belongs to the skills + docs surface, whose paths map derives from @objectstack/spec alone, and spec does not depend on @objectstack/client. A marker on any docs fence importing the SDK therefore red with TS2307: Cannot find module '@objectstack/client' — on correct code. Two pages recorded that constraint in prose and left their SDK blocks deliberately unmarked, so the most SDK-dense page in the docs was structurally unverifiable while #8140's narrowing of 51 client return types falsified two of its examples with CI silent throughout.

Option C, as priced on the card

The spec seat adopted C — a docs root scoped to a page set, on the existing client-SDK surface that already resolves @objectstack/client, @objectstack/client-react and react's real types for its 19 TSDoc blocks. No new resolution environment, no new extraction code. A was rejected (adding the SDK to the spec surface's selfPackages inverts the dependency direction that surface exists to model, and would let any docs page silently acquire the SDK); B was the shipped interim and is now retired by this change.

The card named the one genuine design cost honestly: SourceRoot.dir is a directory, so per-page scoping had to be built. It is SourceRoot.pages — an explicit file list, relative to dir, instead of a walk. The fork-back condition did not trigger: this is a scoped root plus a filter on an existing surface, and the file/marker/tsc pipeline is untouched.

Two roots over one tree brings its own failure mode, so both halves are asserted rather than trusted:

  • Partition, not overlap. The scoped root's pages and the broad root's excludePages read one shared constant (SDK_DOCS_PAGES). assertDisjointSourceFiles() proves per run that no file is scanned by two surfaces — an invariant that held by accident while every root owned its own tree, and whose violation means one page compiled twice in two environments, with the failing verdict unfixable from the page.
  • A page list that stops resolving fails BOTH ways at once. Rename a page and the scoped root loses it while the broad root's subtraction stops matching, handing it back to the surface that cannot resolve it — the original defect, restored silently by an unrelated edit. assertScopedPagesExist() makes that a hard error.

This builds on #12308's shape rather than around it: the REFUSE verdict and the unmarked-population sweep are untouched, and REFUSE correctly named the new docs-sdk root during the measurement below.

Which fences were opted in, and which were not

All 13 fences on api/client-sdk.mdx and both on data-service.mdx were marked, the gate run, and the tree reverted. Readings per fence (source lines are pre-marking):

pagefencereadingopted in
client-sdk.mdx63 Quick Startclean
client-sdk.mdx119, 190, 231, 296, 318, 330TS2304 client (2/13/10/3/5/69 diagnostics)❌ continuation fragments
client-sdk.mdx496 createQueryTS2304 client (1)❌ continuation fragment
client-sdk.mdx516 createFiltercleanthe card's executable criterion
client-sdk.mdx582, 609 error handlingTS2304 + TS18046 (6/4)❌ continuation + unknown catch binding
client-sdk.mdx643 ClientConfigTS2304 Logger (1)❌ fragment
client-sdk.mdx672 React Hooks (tsx)clean
data-service.mdx33 method signaturesdoes not parse (TS1005)❌ signature listing, never compilable
data-service.mdx149 services.data exampleclean

114 diagnostics with all 13+1 marked, and zero TS2307. The pre-change sweep recorded on the card produced 128 including TS2307 on every SDK import. The nine that stay unmarked are the continuation class the page's own note documents: Quick Start establishes client once and each later block reads it. Making those compile would mean injecting casts into prose whose subject IS the real API — teaching worse code than the page teaches now. Both pages' notes are rewritten to record the measurement instead of the retired constraint.

Verification

Every reading below is the gate's own verdict line, at head 31c166e4; the tree was confirmed pristine (git status --porcelain empty) after each reverted measurement.

Fix in placepnpm --filter @objectstack/spec check:skill-examples:

✅ 260 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them
• skills + docs (@objectstack/spec): 227 block(s)
• spec source TSDoc (@objectstack/spec): 10 block(s)
• client SDK (@objectstack/client-react, @objectstack/client): 23 block(s)

256 → 260 marked blocks; the client-SDK surface 19 → 23. The skills + docs count is unchanged at 227, which is the carve-out being a partition rather than a subtraction.

Reverse verification (control). With the four markers left on disk and packages/spec/scripts/check-skill-examples.ts restored to b8419bd5a — mutation confirmed on disk before the run (SDK_DOCS_PAGES occurrences 0, markers still 3+1) — the run reds, and the direction is the card's:

✗ [skills + docs (@objectstack/spec)] examples do not compile:
content/docs/api/client-sdk.mdx:524:30
error TS2307: Cannot find module '@objectstack/client' or its corresponding type declarations.

13 diagnostics: 5× TS2307, plus TS7026/TS2875/TS7006 from the React block meeting a surface with no react types. 524:30 is the createFilter import — the same statement and the same column as the card's 485:30, at its post-#12045 line. Restored with git checkout HEAD -- packages/spec/scripts/check-skill-examples.ts; porcelain clean.

Self-test (--self-test, run as the first half of the gate script) gains page-scope fixtures, including the load-bearing control: an un-partitioned broad root does reach the carved-out page, so the partition assertions are not passing over an empty corpus. It also pins overlap detection and missing-page detection in both directions, and asserts the real SURFACES satisfy both invariants today.

Gate families. Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (34 families, more than the dispatch named — it added check:doc-security-posture, check:docs-redirects, check:docs-single-h1, check:page-declaration-shape, check:published-readme-links, check:react-page-adapter-contract, check:runtime-services-index, check:doc-formula-expressions, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-section-landing-index, check-dev-prereqs, check:docs). 33 of 34 exit 0, each exit code captured before any pipe.

One declared narrowing:node scripts/check-dev-prereqs.mjs exits 1 in this worktree with "The workspace is not built — 34 of 67 workspace packages declare an entry point under dist/ that is not on disk", naming @objectstack/account, @objectstack/setup, @objectstack/studio and 31 others. Only the @objectstack/client-react dependency closure was built here, not the whole workspace. The diff is three files and touches no package.json, no dist/, and no build config, so it cannot reach that gate's predicate; CI runs it after "Build workspace packages".

Follow-up commit 31c166e4 — the guard must skip a root whose dir is absent

CI Test Core went red on packages/spec/scripts/dist-freshness-adoption.test.ts (the #7181 pin suite for this very script), and it was a true red attributable to this diff. assertScopedPagesExist() asserted SDK_DOCS_PAGES unconditionally, but this script is run against repo-shaped sandbox trees as well as against the repo: that suite builds one seeding skills/, packages/spec/src and the two client packages, with no content/docs at all (measured: zero occurrences of "content" in the whole test file). There the guard reported both SDK pages missing and, being an assert that runs before everything else, spoke ahead of the three verdicts those tests pin — hijacking a positive control, a staleness refusal and an orphan-marker finding alike.

sourceFiles() already skips a root whose dir does not exist. The defect was two predicates disagreeing about whether a root is present in this checkout at all — the same shape as the two fence closers (#11690) and the two fence-ownership notions (#11355) that this file has already collapsed into one. The guard now restates that one rule.

The rename protection is untouched, because it answers a different question: content/docs always exists in a real checkout, so a page renamed inside it is still judged and still reds. Absent-dir means "this tree does not carry this corpus", never "this corpus lost a page". Pinned in both directions with a control — an absent dir is silent in both predicates, while the identical page list under a dir that does exist is still flagged, so the exemption is the dir and not a predicate gone quiet.

Post-fix readings at 31c166e4:

✓ check:skill-examples refuses a stale dist (#7181) > POSITIVE CONTROL: a fresh dist type-checks the marked example and reports it
✓ check:skill-examples refuses a stale dist (#7181) > refuses before tsc on a stale dist instead of reporting the examples type-check
✓ check:skill-examples refuses a stale dist (#7181) > lets the dist-independent guards above it speak first, even on a stale dist
Test Files 1 passed (1)
Tests 10 passed (10)

Full @objectstack/spec suite — the suite the red lived in, run in full this time rather than narrowed:

 Test Files 428 passed (428)
Tests 11401 passed (11401)

check:skill-examples on the real corpus is unchanged at 260 marked blocks (227 / 10 / 23), and the 34-family union was re-derived and re-run on this head: 33 of 34 exit 0, with the same single environmental check-dev-prereqs narrowing declared above. The new self-test fixture was ablated (guard line removed, absence confirmed on disk, restored byte-identically via a trap and verified with git hash-object) and fails with exactly its own message — it is load-bearing, not decorative.

⚠️dispatch-gates.mjs reports this branch's tree is 8 commits behind origin/main with 4 derivation inputs changed. Measured rather than shrugged at: lint.yml's change is a pure comment block (#12211's recorded negative result — no gate steps, no globs) and dispatch-gates.mjs is +79/-0 (the staleness warner itself). The derived family list is byte-identical to the pre-merge one, so the union above is not stale in substance.

Changeset

skip-changeset — this PR changes a CI gate script and contributor-facing prose notes; no published package's behaviour or surface moves. Same disposition as #12175 and #12308 on this file.


Generated by Claude Code

`check:skill-examples` resolves modules per SURFACE, and a root was always a
whole subtree — so "which packages may this page import?" was a property of the
tree a page lives in. `content/docs/**` resolves against `@objectstack/spec`
alone, and spec does not depend on `@objectstack/client`, so a marker on any
docs fence importing the SDK red with TS2307 on correct code. Two pages
recorded that in prose and left their SDK blocks deliberately unmarked.
`SourceRoot.pages` makes a root a page SET rather than a tree, so the two SDK
pages join the client-SDK surface that already resolves those packages for its
19 TSDoc blocks — no new resolution environment and no new extraction code.
The scoped root's `pages` and the broad root's `excludePages` read one shared
constant, and two guards prove the partition every run: no file may be scanned
by two surfaces, and every scoped page must exist.
Opts in the four fences that are self-contained (Quick Start, the
`createFilter()` chain and the React Hooks block on the SDK page; the
`services.data` example on the data-service page). The remaining nine are
continuation fragments that read Quick Start's `client` and stay unmarked; both
pages' notes now record the measurement rather than the old constraint.
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tooling labels Aug 25, 2026
@os-litantos-litant added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed documentation Improvements or additions to documentation tooling labels Aug 25, 2026 — with Claude
@github-actions

github-actionsBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

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

`assertScopedPagesExist()` asserted SDK_DOCS_PAGES unconditionally, but this
script is run against repo-SHAPED sandbox trees as well as the repo: the #7181
dist-freshness pins build one that seeds skills/, packages/spec/src and the two
client packages and has no content/docs at all. There the guard reported both
SDK pages missing and — being an assert that runs before everything else —
spoke ahead of the three verdicts those tests pin, hijacking a positive
control, a staleness refusal and an orphan-marker finding alike.
`sourceFiles()` already skips a root whose `dir` does not exist. The defect was
two predicates disagreeing about whether a root is present in this checkout at
all, the same shape as the two fence closers (#11690) and the two
fence-ownership notions (#11355) this file has already collapsed. The guard now
restates that one rule.
The rename protection is untouched: content/docs always exists in a real
checkout, so a page renamed inside it is still judged and still reds. Pinned in
both directions, with a control — an absent dir is silent in BOTH predicates,
while the identical page list under a dir that DOES exist is still flagged, so
the exemption is the dir and not a predicate gone quiet.
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tooling and removed size/m labels Aug 25, 2026
@os-litant
os-litant marked this pull request as ready for review August 25, 2026 20:37
@os-litant
os-litant added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit d516483Aug 25, 2026
37 checks passed
@os-litant
os-litant deleted the claude/issue-12048-sdk-docs-surface-resolution branch August 25, 2026 20:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check:skill-examples — the docs surface cannot resolve @objectstack/client, so no SDK docs page can ever be opted in

2 participants

@os-litant@claude