diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx index e5309f72f3..032275974a 100644 --- a/content/docs/api/client-sdk.mdx +++ b/content/docs/api/client-sdk.mdx @@ -24,42 +24,48 @@ pnpm add @objectstack/client ``` {/* - CONTRIBUTOR NOTE — none of this page's TypeScript fences carries an - `os:check` marker, and that is a recorded decision rather than an omission. - - `check:skill-examples` compiles marked blocks per SURFACE. `content/docs/**` - belongs to the "skills + docs" surface, whose resolution dir and `paths` map - are derived from `@objectstack/spec` alone — and `@objectstack/spec` does not - depend on `@objectstack/client`. So a marker on any fence that imports the SDK - reds with TS2307 "Cannot find module '@objectstack/client'" — a - surface-resolution gap, not doc-vs-SDK drift. - - Every other fence is a deliberate continuation fragment: Quick Start - establishes `client` once and each later block continues that implied context, - so a marker there reds with TS2304 "Cannot find name 'client'". Making either - class compile would mean hand-declaring the SDK's own types, or injecting - casts into prose whose subject IS the real API — pinning each example to - itself and teaching worse code than the page teaches now. - - Measured on this page (all 13 fences marked, then reverted): 128 diagnostics, - every one TS2307 / TS2304 / TS18004 / TS18046 / TS2591 / TS7006 / TS7026 / - TS2875. Not one was a doc-vs-SDK divergence. - - The React Hooks block near the end is fenced tsx, not typescript, because it - IS JSX: the gate writes every block out with its fence's own extension, and - JSX in a .ts file is a syntax error. That reaches past this page — tsc stops - at syntax errors and never runs the semantic pass, so one such block would - suppress type-checking for every marked block across skills/ and - content/docs/ (measured here: 128 semantic diagnostics collapse to 0). Please - do not retag it back. - - The marker becomes worth adding here the day the docs surface can resolve - `@objectstack/client` — the same condition recorded in - content/docs/kernel/runtime-services/data-service.mdx. + CONTRIBUTOR NOTE — three of this page's fences carry an `os:check` marker and + are compiled by `check:skill-examples` against the real SDK declarations; the + rest deliberately do not. Both halves are measured, not assumed. + + This page belongs to the client-SDK SURFACE, not to the broad + `content/docs/**` one. That surface resolves `@objectstack/client`, + `@objectstack/client-react` and react's real types, so a marked block here + imports the SDK exactly as a reader's own code does. It was not always so: + `content/docs/**` resolves against `@objectstack/spec` alone, spec does not + depend on the client, and every SDK import here used to red with TS2307 + "Cannot find module '@objectstack/client'". The page is carved out by name + (`SDK_DOCS_PAGES` in packages/spec/scripts/check-skill-examples.ts) — moving + or renaming it without updating that list fails the gate loudly rather than + silently returning the page to the surface that cannot resolve it. + + What IS marked: Quick Start, the `createFilter()` builder chain, and the + React Hooks block — the three that stand alone. What is not: every block that + continues Quick Start's implied context. Quick Start establishes `client` + once and each later block reads it, so a marker there reds with TS2304 + "Cannot find name 'client'"; the error-handling blocks additionally read a + `catch` binding that is `unknown` (TS18046). Making those compile would mean + hand-declaring the SDK's own types or injecting casts into prose whose + subject IS the real API — pinning each example to itself and teaching worse + code than the page teaches now. + + Measured with all 13 fences marked, then reverted: 114 diagnostics, TS2304 / + TS18046 / TS18004 / TS2591, spread over the nine continuation blocks — and + ZERO TS2307. Before the carve-out the same sweep produced 128 diagnostics + including TS2307 on every SDK import. Not one diagnostic in either sweep was + a doc-vs-SDK divergence; that is what the three marked blocks now hold. + + The React Hooks block is fenced tsx, not typescript, because it IS JSX: the + gate writes every block out with its fence's own extension, and JSX in a .ts + file is a syntax error. That reaches past this page — tsc stops at syntax + errors and never runs the semantic pass, so one such block would suppress + type-checking for every marked block on its whole surface. Please do not + retag it back. */} ## Quick Start +{/* os:check */} ```typescript import { ObjectStackClient } from '@objectstack/client'; @@ -513,6 +519,7 @@ const results = await client.data.query('account', query); The `FilterBuilder` provides a rich set of filter methods: +{/* os:check */} ```typescript import { createFilter } from '@objectstack/client'; @@ -669,6 +676,7 @@ For React applications, use `@objectstack/client-react`: pnpm add @objectstack/client-react ``` +{/* os:check */} ```tsx import { ObjectStackProvider, useClient, useQuery } from '@objectstack/client-react'; import { ObjectStackClient } from '@objectstack/client'; diff --git a/content/docs/kernel/runtime-services/data-service.mdx b/content/docs/kernel/runtime-services/data-service.mdx index 86c7904a07..e281166139 100644 --- a/content/docs/kernel/runtime-services/data-service.mdx +++ b/content/docs/kernel/runtime-services/data-service.mdx @@ -146,6 +146,7 @@ Call these methods from code that **holds** the binding — a managed runtime ha a hook body: a hook has no `services` key to reach through (see above), and reads other objects via `ctx.api`. +{/* os:check */} ```ts import type { ObjectStackClient } from '@objectstack/client'; @@ -179,11 +180,17 @@ export async function recentOrdersForContact(data: DataService, contactId: strin } ``` -The block carries no `{/* os:check */}` marker, and that is a measurement rather than an -omission: `check:skill-examples` compiles marked blocks against the built -`@objectstack/spec` declarations only — its `paths` map is derived from that package's own -`exports`, and `@objectstack/spec` does not depend on `@objectstack/client`. A marked -block here would therefore have to hand-declare `DataService` instead of importing it, -which pins the example to itself and nothing else. The marker becomes worth adding the day -this surface has a spec-side contract to import (see the -[Canonical source](#canonical-source) note). +The block carries an `{/* os:check */}` marker, so `check:skill-examples` compiles it +against the real `@objectstack/client` declarations on every CI run: if `data.get` / +`data.find` / `data.query` change shape, this example reds rather than rotting. It used to +carry no marker, and that was a measurement rather than an omission — marked blocks under +`content/docs/**` were compiled against `@objectstack/spec` alone, which does not depend +on `@objectstack/client`, so importing `ObjectStackClient` here red with TS2307 and +hand-declaring `DataService` instead would have pinned the example to itself and nothing +else. This page is now carved out by name to the client-SDK surface (`SDK_DOCS_PAGES` in +`packages/spec/scripts/check-skill-examples.ts`), which resolves the SDK; renaming or +moving the page without updating that list fails the gate rather than silently dropping +the check. + +The signature listing under [Methods](#methods) stays unmarked: it is a listing of method +shapes, not a compilable statement, and it cannot parse at all. diff --git a/packages/spec/scripts/check-skill-examples.ts b/packages/spec/scripts/check-skill-examples.ts index a955b964b9..8f27bec1dd 100644 --- a/packages/spec/scripts/check-skill-examples.ts +++ b/packages/spec/scripts/check-skill-examples.ts @@ -183,6 +183,53 @@ * real consumer would, rather than relying on a same-package self-import * trick that may or may not resolve. * + * ── PAGE-SCOPED ROOTS: one tree, two surfaces (#12048) ──────────────────── + * A surface is a resolution environment, and until #12048 a ROOT was always a + * whole subtree — which made "which packages can this page import?" a property + * of the tree a page happens to live in. `content/docs/**` is one tree, so + * every page in it resolved `@objectstack/spec` and nothing else. The SDK + * reference page (`api/client-sdk.mdx`) and the data-service page both import + * `@objectstack/client`; spec does not depend on the client, so a marker on + * either page red with `TS2307: Cannot find module '@objectstack/client'` on + * correct code. Both pages recorded that in prose and left their SDK blocks + * deliberately unmarked — two pages structurally unverifiable, for one + * resolution gap, while #8140's narrowing of 51 client return types falsified + * two examples on the SDK page with CI silent throughout. + * + * The fix is a root whose scope is a PAGE SET rather than a tree + * (`SourceRoot.pages`), on the client-SDK surface that already resolves those + * packages for its 19 TSDoc blocks. No new resolution environment, no new + * extraction code — the same file/marker/tsc pipeline, pointed at two named + * pages. The alternative (teaching the spec surface to resolve the SDK) was + * rejected: it inverts the dependency direction that surface exists to model, + * and would let any docs page silently acquire the SDK. + * + * Two roots over ONE tree brings its own failure mode, and both halves are + * asserted rather than trusted: + * + * - **Partition, not overlap.** A page scanned by two surfaces is compiled + * twice in two environments, and the one that cannot resolve its imports + * reds against a page the other just proved correct — unfixable from the + * page. The scoped root's `pages` and the broad root's `excludePages` read + * ONE shared constant (`SDK_DOCS_PAGES`), and + * `assertDisjointSourceFiles()` proves per run that no file is claimed + * twice — an invariant that held by accident while every root owned its + * own tree. + * - **A page list that stops resolving fails BOTH ways at once.** Rename the + * page and the scoped root loses it while the broad root's subtraction + * stops matching, so it lands back on the surface that cannot resolve it — + * the original defect, restored silently by an unrelated edit. + * `assertScopedPagesExist()` makes that a hard error. + * - **But "this tree has no such corpus" is not "this corpus lost a page".** + * This script is run against repo-SHAPED sandbox trees too (the #7181 + * dist-freshness pins build one with no `content/docs` at all), so the + * missing-page guard skips a root whose `dir` is absent — the same + * presence rule `sourceFiles()` already applies. When the two predicates + * disagreed on that, the guard reported both SDK pages missing in the + * sandbox and, being an assert that runs first, spoke ahead of the three + * verdicts those tests pin. `content/docs` always exists in a real + * checkout, so the rename protection above is untouched. + * * ── Fence-awareness, in BOTH of extractFromFile's loops ─────────────────── * `fenceOwners()` tracks every top-level fence of ANY language (lifted from * the #10533 fence-awareness shape in `scripts/check-role-word.mjs`), and both @@ -257,6 +304,35 @@ interface SourceRoot { /** Skip individual files by basename — e.g. test files, which are not the * documented SDK surface even though they share the source root's `ext`. */ excludeFile?: (name: string) => boolean; + /** + * PAGE-SCOPED root (#12048): an explicit file list, relative to `dir`, + * INSTEAD of walking the tree. A root with `pages` is a page SET, not a + * subtree — which is what lets one page of a shared prose tree belong to a + * different surface (a different module-resolution environment) from the + * tree around it, without moving the file or forking the extractor. + * + * `dir` still matters: it is what `buildFileName` takes each page's path + * relative to, so two roots over the same tree produce stable, distinct + * build-file names as long as their `label`s differ. + * + * `exclude` / `excludeFile` are not consulted for a `pages` root — the list + * IS the scope. Every entry must exist (`assertScopedPagesExist()`): a + * renamed page would otherwise drop out of its scoped root silently and be + * picked back up by the broad root it was carved out of, quietly restoring + * the very resolution gap the carve-out exists to close. + */ + pages?: string[]; + /** + * The complement of another root's `pages` (#12048): files under `dir` + * (relative paths) this root does NOT scan because a page-scoped root on + * another surface owns them. Both sides read ONE shared constant, so the + * partition cannot drift — and `assertDisjointSourceFiles()` fails the run + * if it ever does. Overlap is not cosmetic: a page scanned by two surfaces + * has every marked block on it compiled twice, in two different resolution + * environments, and the surface that cannot resolve its imports reports + * TS2307 against a page the other surface just proved correct. + */ + excludePages?: string[]; /** True when this root's prose lives inside a JSDoc/TSDoc block comment * rather than free-standing markdown — every line then carries a leading * JSDoc gutter (` * `) that must be stripped before the fence/marker @@ -280,10 +356,48 @@ interface SourceRoot { * `content/docs/references/` is excluded: `build-docs.ts` regenerates it from the * schemas, so its snippets cannot drift independently of their source. */ +const DOCS_DIR = path.resolve(REPO_ROOT, 'content/docs'); + +/** + * The docs pages whose examples import the CLIENT SDK, and so belong to the + * client-SDK surface's resolution environment rather than the spec surface's + * (#12048). Paths are relative to `content/docs`. + * + * ONE constant, read from both sides of the partition: the spec surface's + * broad docs root subtracts it (`excludePages`), the client-SDK surface's + * page-scoped docs root is exactly it (`pages`). Declaring it twice by hand is + * what would let the two drift into overlap or into a gap; declaring it once + * makes the partition true by construction, and `assertDisjointSourceFiles()` + * proves it every run rather than trusting this comment. + * + * WHY these two pages and not the whole tree. `content/docs/**` is resolved + * against `@objectstack/spec` alone, and spec does not depend on + * `@objectstack/client` — so before this list existed, a marker on ANY fence + * importing the SDK red with `TS2307: Cannot find module '@objectstack/client'`, + * and both pages below recorded that in prose and left their SDK blocks + * deliberately unmarked. Two pages structurally unverifiable, for one + * resolution gap, while #8140's narrowing of 51 client return types falsified + * two examples on `api/client-sdk.mdx` with CI silent throughout. + * + * The alternative — teaching the spec surface to resolve the SDK — was + * rejected: it inverts the dependency direction that surface exists to model + * (spec knows nothing of its consumers) and would let ANY docs page silently + * acquire the SDK. This list is the opposite posture: a page opts IN, by name, + * to a resolution environment that already exists and is already exercised by + * the 19 TSDoc blocks in `packages/client{,-react}/src`. + * + * Adding a page here moves the WHOLE file between surfaces, so add one only + * when its examples' imports are SDK-shaped. A page needing both `@objectstack/spec` + * and the SDK is fine — client-react depends on spec, so spec resolves through + * this surface's `node_modules` too; a page needing something NEITHER surface + * carries is a new surface question, not a new entry here. + */ +const SDK_DOCS_PAGES = ['api/client-sdk.mdx', 'kernel/runtime-services/data-service.mdx']; + const SKILLS_DOCS_ROOTS: SourceRoot[] = [ { dir: path.resolve(REPO_ROOT, 'skills'), ext: '.md', label: 'skills', marker: '' }, { - dir: path.resolve(REPO_ROOT, 'content/docs'), + dir: DOCS_DIR, ext: '.mdx', label: 'docs', // MDX has no HTML comments — fumadocs-mdx fails the build outright on @@ -291,6 +405,8 @@ const SKILLS_DOCS_ROOTS: SourceRoot[] = [ // `{/* text */}`"). The marker must follow each format's own comment syntax. marker: '{/* os:check */}', exclude: [path.resolve(REPO_ROOT, 'content/docs/references')], + // Carved out to the client-SDK surface (#12048) — see `SDK_DOCS_PAGES`. + excludePages: SDK_DOCS_PAGES, }, ]; @@ -353,6 +469,25 @@ const CLIENT_SDK_ROOTS: SourceRoot[] = [ commentPrefixed: true, excludeFile: isTestFile, }, + /** + * The SDK's PROSE pages (#12048) — free-standing MDX, not TSDoc, so no + * `commentPrefixed` gutter and the MDX marker spelling, exactly like the + * broad docs root it is carved out of. What differs is the surface it sits + * on, and that is the whole point: these pages resolve `@objectstack/client` + * (and `client-react`, and react's real types) because this surface's + * throwaway build dir lives inside `packages/client-react/`. + * + * A distinct `label` from the broad docs root is load-bearing, not cosmetic: + * `buildFileName` is `