diff --git a/.changeset/9412-ungated-docs-paydown.md b/.changeset/9412-ungated-docs-paydown.md new file mode 100644 index 0000000000..091094a533 --- /dev/null +++ b/.changeset/9412-ungated-docs-paydown.md @@ -0,0 +1,27 @@ +--- +--- + +Pay down the three `UNGATED_DOCS` rows objectui#7308 opened for the nested +package READMEs, so `scripts/check-doc-snippet-types.mjs` compiles all three +pages instead of declaring them unverified. + +The one reader-visible defect is on `packages/core/src/adapters/README.md`: the +custom-adapter template declared `implements DataSource` while omitting +`getObjectSchema`, which the interface requires, and wrote `// Your +implementation` as the whole body of six methods annotated non-`void`. A reader +who copied it got a class that does not satisfy the interface it claims. The +template now carries all six required members and throws from each +unimplemented body, so it type-checks at every step of being filled in. + +`packages/types/src/zod/README.md` had two `{ ... }` elisions TypeScript reads +as a spread with no operand, six excerpts continuing an earlier block's imports, +one fence holding a before-and-after pair that declared the same two names +twice, and a shape sketch fenced as TypeScript. `packages/components/src/__tests__/README.md` +gains the imports a file in that directory really writes, and a fragment +declaration saying why a probe compiled at the repository root cannot resolve +either of them. + +No published file changes: none of the three pages is inside any package +tarball — each package's manifest `files` list carries `dist` and the +package-root `README.md`, and `npm pack --dry-run` reports zero entries under +`src/` for all three. Gate script and its own test suite only, otherwise. diff --git a/packages/components/src/__tests__/README.md b/packages/components/src/__tests__/README.md index 9a45b5ae3e..98868b2438 100644 --- a/packages/components/src/__tests__/README.md +++ b/packages/components/src/__tests__/README.md @@ -99,9 +99,19 @@ The failing tests have discovered legitimate issues with component schemas and p ## Adding New Tests -When adding a new component, follow this pattern: +When adding a new component, follow this pattern — the imports are the ones a +file in this directory writes, so copy the block whole: + ```typescript +import { describe, it, expect } from 'vitest'; + +import { + getAllDisplayIssues, + renderComponent, + validateComponentRegistration, +} from './test-utils'; + describe('NewComponent Renderer', () => { it('should be properly registered', () => { const validation = validateComponentRegistration('new-component'); diff --git a/packages/core/src/adapters/README.md b/packages/core/src/adapters/README.md index e65b2d84cf..fca6f2c99e 100644 --- a/packages/core/src/adapters/README.md +++ b/packages/core/src/adapters/README.md @@ -190,6 +190,11 @@ renderer calls; components do not branch on `provider` themselves. ```typescript import { resolveDataSource } from '@object-ui/core'; +import type { DataSource } from '@object-ui/types'; + +// The `DataSource` the renderer already holds from context. It is what +// `provider: 'object'` resolves to, and the fallback for every other case. +declare const contextDataSource: DataSource; const dataSource = resolveDataSource( { provider: 'api', read: { url: '/api/users' } }, @@ -215,6 +220,10 @@ stay ignorant of which one ran. ```typescript import { runBatchTransaction } from '@object-ui/core'; +import type { DataSource } from '@object-ui/types'; + +// The adapter the view resolved to — see `resolveDataSource` above. +declare const dataSource: DataSource; // `{ $ref: 0 }` resolves to the id minted by operation 0 (the parent). await runBatchTransaction(dataSource, [ @@ -234,39 +243,79 @@ for the capability negotiation that decides which path is taken. ## Creating Custom Adapters -To create a custom adapter, implement the `DataSource` interface: +To create a custom adapter, implement the `DataSource` interface. It requires +**six** members — `find`, `findOne`, `create`, `update`, `delete` and +`getObjectSchema` — and everything else on it is optional. `getObjectSchema` is +easy to miss and is not optional: schema-dependent components call it before they +render, which is why `ApiDataSource` answers it with a minimal stub rather than +omitting it. ```typescript import type { DataSource, QueryParams, QueryResult } from '@object-ui/types'; export class MyCustomAdapter implements DataSource { + // ── The six members `DataSource` requires ─────────────────────────────── + async find(resource: string, params?: QueryParams): Promise> { - // Your implementation + throw new Error(`find(${resource}) is not implemented yet`); } - - async findOne(resource: string, id: string | number): Promise { - // Your implementation + + async findOne( + resource: string, + id: string | number, + params?: QueryParams, + ): Promise { + throw new Error(`findOne(${resource}, ${id}) is not implemented yet`); } - + async create(resource: string, data: Partial): Promise { - // Your implementation + throw new Error(`create(${resource}) is not implemented yet`); + } + + async update( + resource: string, + id: string, + data: Partial, + opts?: { ifMatch?: string }, + ): Promise { + throw new Error(`update(${resource}, ${id}) is not implemented yet`); } - - async update(resource: string, id: string | number, data: Partial): Promise { - // Your implementation + + async delete( + resource: string, + id: string | number, + opts?: { ifMatch?: string }, + ): Promise { + throw new Error(`delete(${resource}, ${id}) is not implemented yet`); } - - async delete(resource: string, id: string | number): Promise { - // Your implementation + + /** + * Required. Return the object's metadata, or a minimal stub + * (`{ name, fields: {} }`) when your backend exposes none — see + * `ApiDataSource` above. + */ + async getObjectSchema(objectName: string): Promise { + return { name: objectName, fields: {} }; } - - // Optional: bulk operations - async bulk?(resource: string, operation: string, data: Partial[]): Promise { - // Your implementation + + // ── Optional: implement only what your backend actually supports ─────────── + + async bulk?( + resource: string, + operation: 'create' | 'update' | 'delete', + data: Partial[], + ): Promise { + throw new Error(`bulk(${resource}, ${operation}) is not implemented yet`); } } ``` +The bodies above **throw** rather than fall off the end: a method annotated +`Promise>` that returns nothing is a type error, and a template +that does not type-check is one a reader copies into a class that does not +satisfy the interface it claims to implement. Replace each `throw` as you go and +the class stays checkable at every step. + ## Related Packages - `@object-ui/types` — the `DataSource`, `QueryParams` and `ViewData` definitions these adapters implement diff --git a/packages/types/src/zod/README.md b/packages/types/src/zod/README.md index 07a12c6b9c..80b62c727a 100644 --- a/packages/types/src/zod/README.md +++ b/packages/types/src/zod/README.md @@ -217,7 +217,7 @@ function validateComponent(config: unknown) { All component schemas follow the @objectstack/spec UI specification format: -```typescript +```text { // Required type: string, // Component type identifier @@ -243,6 +243,8 @@ All component schemas follow the @objectstack/spec UI specification format: ### Error Messages ```typescript +import { ButtonSchema } from '@object-ui/types/zod'; + const result = ButtonSchema.safeParse({ type: 'button', variant: 'invalid-variant' @@ -264,6 +266,8 @@ const result = ButtonSchema.safeParse({ ### Nested Validation ```typescript +import { CardSchema } from '@object-ui/types/zod'; + // Validates nested components in Card const cardWithChildren = CardSchema.parse({ type: 'card', @@ -279,6 +283,11 @@ const cardWithChildren = CardSchema.parse({ 1. **Use safeParse()** for user input validation ```typescript + import { ButtonSchema } from '@object-ui/types/zod'; + + // Whatever arrived from the form, the request body or the config file. + declare const userInput: unknown; + const result = ButtonSchema.safeParse(userInput); if (!result.success) { // Handle errors gracefully @@ -287,6 +296,11 @@ const cardWithChildren = CardSchema.parse({ 2. **Use parse()** for internal configurations ```typescript + import { ButtonSchema } from '@object-ui/types/zod'; + + // A configuration your own code produced, so a throw is the right failure. + declare const internalConfig: unknown; + // Throws error on invalid data const config = ButtonSchema.parse(internalConfig); ``` @@ -301,10 +315,10 @@ const cardWithChildren = CardSchema.parse({ ```typescript import type { ButtonSchema as ButtonType } from '@object-ui/types'; import { ButtonSchema } from '@object-ui/types/zod'; - + // Use type for declarations - const config: ButtonType = { ... }; - + const config: ButtonType = { type: 'button', label: 'Save', variant: 'default' }; + // Use schema for validation ButtonSchema.parse(config); ``` @@ -322,9 +336,15 @@ Zod schemas are designed for runtime validation: ### With React Hook Form ```typescript -import { zodResolver } from '@hookform/resolvers/zod'; import { FormSchema } from '@object-ui/types/zod'; +// `react-hook-form` and `@hookform/resolvers` are YOUR app's dependencies, not +// this package's. These two stand in for `import { useForm } from +// 'react-hook-form'` and `import { zodResolver } from '@hookform/resolvers/zod'` +// so the schema half below is still checked against the shipped types. +declare function useForm(options: { resolver: unknown }): unknown; +declare function zodResolver(schema: unknown): unknown; + const form = useForm({ resolver: zodResolver(FormSchema), }); @@ -354,6 +374,9 @@ export async function POST(req: Request) { ```typescript import { AnyComponentSchema } from '@object-ui/types/zod'; +// Your own store of validated configurations. +declare const registry: Map; + function registerComponent(config: unknown) { // Validate before registration const validated = AnyComponentSchema.parse(config); @@ -365,16 +388,22 @@ function registerComponent(config: unknown) { If you're currently using only TypeScript types: +Before — the type alone, checked only where the literal is written: + ```typescript -// Before (TypeScript only) import type { ButtonSchema } from '@object-ui/types'; -const button: ButtonSchema = { ... }; -// After (with runtime validation) +const button: ButtonSchema = { type: 'button', label: 'Save' }; +``` + +After — the same literal, plus a runtime check at the boundary. Import the type +under an alias, because the Zod twin ships under the same name: + +```typescript import type { ButtonSchema as ButtonType } from '@object-ui/types'; import { ButtonSchema } from '@object-ui/types/zod'; -const button: ButtonType = { ... }; +const button: ButtonType = { type: 'button', label: 'Save' }; const validated = ButtonSchema.parse(button); ``` diff --git a/scripts/__tests__/check-doc-snippet-types.test.ts b/scripts/__tests__/check-doc-snippet-types.test.ts index 557cf6ff62..f98cdda521 100644 --- a/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/scripts/__tests__/check-doc-snippet-types.test.ts @@ -984,7 +984,21 @@ describe('objectui#7308 — the nested package READMEs are in the scan set, ledg expect(new Set(walked).size).toBe(walked.length); }); - it('the widening is VISIBLE to the accounting: block-bearing pages are ledgered, block-free ones are covered', () => { + /** + * objectui#9412 paid the three rows down, so this pin's direction INVERTED: + * where it used to say "every block-bearing nested page is on the ledger", the + * state it now holds is that NONE of them is, and that every one of them is + * covered. Both readings are the same claim about the accounting — the + * widening is visible in it — and the half that was never about the debt is + * kept verbatim: a nested page with no ts/tsx block is covered at zero blocks + * and may not be ledgered. + * + * ⛔ The inversion is not a relaxation. A ledgered nested page would still be + * legal the day somebody writes a row with a reason (`analyze` re-derives every + * row), and the sibling case below is what keeps the row's SHAPE requirement + * live for that day. + */ + it('the widening is VISIBLE to the accounting: every nested page is covered, none is ledgered', () => { const state = analyze({}) as { scans: Map; covered: string[]; @@ -992,31 +1006,48 @@ describe('objectui#7308 — the nested package READMEs are in the scan set, ledg const nested = nestedPackageReadmePages(repoRoot); const withBlocks = nested.filter((doc) => (state.scans.get(doc)?.blocks.length ?? 0) > 0); const withoutBlocks = nested.filter((doc) => (state.scans.get(doc)?.blocks.length ?? 0) === 0); - // Non-vacuous on BOTH halves — this is the assertion that says why three rows - // were written for four pages. + // Non-vacuous on BOTH halves — a nested page that really holds blocks, and a + // nested page that really holds none, are each present in the tree. expect(withBlocks.length).toBeGreaterThan(0); expect(withoutBlocks.length).toBeGreaterThan(0); - expect([...withBlocks].sort()).toEqual( - Object.keys(UNGATED_DOCS as Record) - .filter((doc) => nested.includes(doc)) - .sort(), - ); + // The debt is paid: no nested README is ungated any more. + expect(Object.keys(UNGATED_DOCS as Record).filter((doc) => nested.includes(doc))).toEqual([]); // A page with no ts/tsx block is COVERED at zero blocks and may not be // ledgered: the stale-entry check would refuse it, which is exactly why // leaving it out of the surface to keep the ledger short is not available. - for (const doc of withoutBlocks) expect(state.covered).toContain(doc); + // The block-bearing ones are covered now too, and the gate compiles them. + for (const doc of nested) expect(state.covered).toContain(doc); }); - it('every new ledger row carries a measured count, the phases, and what would have to change', () => { + it('every ledger row a nested page might get still owes a measured count, the phases, and what would have to change', () => { + const shapeFailures = (reason: string) => + [ + [/\d+ `tsx?` blocks?/, 'names no block count'], + [/\d+ diagnostics/, 'names no diagnostic count'], + [/TS\d{4}/, 'names no diagnostic code'], + [/syntax-phase|semantic-phase/, 'does not say which phase was measured'], + [/What would have to change|would have to change/, 'does not say what would have to change'], + ].flatMap(([pattern, complaint]) => ((pattern as RegExp).test(reason) ? [] : [complaint as string])); + + // Non-vacuity, in place of the population this used to loop over: the shape + // checker itself is exercised against a row that satisfies it and one that + // does not, so a nested row reappearing cannot land on a check that has + // quietly stopped checking anything. + expect( + shapeFailures( + '2 `ts` blocks, 4 diagnostics, ALL semantic-phase: TS2304 x4. What would have to change: the ' + + 'excerpts declare the values they use.', + ), + ).toEqual([]); + expect(shapeFailures('this page does not compile')).toHaveLength(5); + + // Today: the nested leg carries no ledger row at all (objectui#9412). The + // loop below is what applies the shape the day one returns. const nested = new Set(nestedPackageReadmePages(repoRoot)); const entries = Object.entries(UNGATED_DOCS as Record).filter(([doc]) => nested.has(doc)); - expect(entries.length).toBeGreaterThan(0); + expect(entries).toEqual([]); for (const [doc, reason] of entries) { - expect(reason, `${doc}: names no block count`).toMatch(/\d+ `tsx?` blocks?/); - expect(reason, `${doc}: names no diagnostic count`).toMatch(/\d+ diagnostics/); - expect(reason, `${doc}: names no diagnostic code`).toMatch(/TS\d{4}/); - expect(reason, `${doc}: does not say which phase was measured`).toMatch(/syntax-phase|semantic-phase/); - expect(reason, `${doc}: does not say what would have to change`).toMatch(/What would have to change|would have to change/); + expect(shapeFailures(reason), `${doc}: ${shapeFailures(reason).join('; ')}`).toEqual([]); } }); diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index 8265663f7d..ae23012253 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -1031,6 +1031,56 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * stays here because this ledger keeps the record of why each declaration * existed, not because the page still carries them. * + * Batch 5 (objectui#9412) paid down objectui#7308's three NESTED package-README + * rows, the whole of that card's debt half. Re-derived first on `8196b10631` + * with this gate's own analyzer, the rows temporarily lifted — `analyze({ ungated + * })` for the population, `compileSnippets()` for the phases, over the closure + * `--build-filter` names (35/35 turbo tasks successful) — and every figure the + * rows recorded on `9ba7e9c3` still held: 20 blocks, 13 failing (3 syntax-phase, + * 10 semantic), 37 diagnostics, split across the pages exactly as written. In + * the same runs the sentinel produced TS2305, the positive control 0, and both + * bound controls TS2307, so the zeros below are readings from a program that + * demonstrably reports non-zero. + * + * Its defect was the one objectui#7308 named first, and it is the kind this gate + * exists for: `packages/core/src/adapters/README.md`'s custom-adapter template + * declared `implements DataSource` while omitting `getObjectSchema` (TS2420), + * and wrote `// Your implementation` as the whole body of six methods annotated + * non-`void` (TS2355 x6). A reader who copied it got a class that does not + * satisfy the interface it claims. The template now implements all six REQUIRED + * members — `find`, `findOne`, `create`, `update`, `delete`, `getObjectSchema` — + * and each unimplemented body throws rather than falling off the end, so the + * reader's class type-checks at every step of filling it in. + * + * Routes, in the two the batches above established: 19 blocks compile (the two + * `{ ... }` elisions on the zod page written as real initialisers, six excerpts + * given their own imports or a `declare const` stand-in, one before/after fence + * split into the two programs it was really holding, and the shape sketch + * re-fenced ```text, which takes it out of the ts/tsx population), and ONE block + * is a declared fragment: `packages/components/src/__tests__/README.md`'s + * "Adding New Tests" pattern. Both of its specifiers were measured refused in + * this program before the marker was written — `vitest` is the ROOT-DECLARED + * control specifier itself, so the row's own first remedy ("the block imports + * `describe`/`it`/`expect` from `vitest`") produces a `[bound]` failure by + * construction, and `./test-utils` is TS2307 because every block compiles at the + * repository root while that helper is suite-local and unshipped + * (`@object-ui/components` lists `dist` in `files`, and `dist/` holds no + * `test-utils`). The row anticipated exactly that and named the marker as its + * alternative. The block imports the two specifiers anyway, because they are the + * ones a file in that directory really writes, and a stand-in would have taught a + * spelling nobody should copy; the marker costs no coverage here, since the block + * imports no documented package surface at all. + * + * ⚠️ One claim in the retired zod row was FALSE and is corrected rather than + * carried forward: it said `packages/types` "lists the whole of `src/` in its + * manifest `files`", so `src/zod/README.md` ships in the npm tarball. It does + * not. That manifest's `files` is `['dist', 'README.md', 'CHANGELOG.md', + * 'LICENSE']`, and `npm pack --dry-run --json` in that package reports 134 + * entries, none of them under `src/` and exactly one README — the package-root + * one. The page was still worth clearing, on the reason every other row here + * gives: it is a page a reader copies from. It is not worth clearing because it + * ships, and a later card should not plan around that. + * * objectui#5343 then read that list back and cleared it for the getting-started * pages: no entry for `content/docs/guide/**` or for * `content/docs/api/schema-reference.md` names a missing export any more. Every @@ -1057,66 +1107,9 @@ const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); * @type {Record} */ const UNGATED_DOCS = { - // objectui#7308 — the nested package READMEs, LEDGER-FIRST. Measured on - // `9ba7e9c3` with this gate's own analyzer against the closure `--build-filter` - // names (35/35 turbo tasks successful): `analyze({ ungated: {} })` for the - // population, `compileSnippets()` for the phases, with the surface widened and - // these rows NOT yet written — which is the only order in which the numbers - // below are readings rather than justifications. - // - // The census, with each population named: the widening adds 4 documents - // (245 -> 249 in the scan set) carrying 20 `ts`/`tsx` blocks, of which 13 fail - // — 3 in the syntax phase and 10 in the semantic phase — for 37 diagnostics. - // - // ⚠️ Only THREE rows appear below for those four documents, and the missing - // fourth is mechanical rather than an exclusion: `packages/plugin-gantt/docs/ - // verification/README.md` holds no `ts`/`tsx` fenced block at all (its fences - // are `sh`), so it joins the COVERED tier at zero blocks, and a row naming it - // here would fail this gate's own re-derivation as a stale entry — "an entry - // naming a file that ... holds no `ts` / `tsx` block at all, fails as a stale - // entry". ⛔ That is the opposite of keeping the ledger short by quietly leaving - // a page outside the surface, which is the defect objectui#7308 reported. - // - // ⚠️ These three rows are DEBT, not a terminal state — unlike the objectui#7856 - // card 2 rows below, which are records nobody may repair. A package README is a - // page a reader copies from, so every row here can and should leave by the page - // compiling. Paying it down is its own card; ⛔ softening this gate is not a way - // to pay it. - 'packages/components/src/__tests__/README.md': - '1 `ts` block (fence 104), 9 diagnostics, ALL semantic-phase: TS2593 x3, TS2304 x5, TS2552 x1. The ' + - "block is the page's \"Adding New Tests\" pattern: a bare `describe`/`it` body with no imports at all, " + - 'so the Vitest globals (`describe`, `it`, `expect`) and the three local helpers it calls ' + - '(`validateComponentRegistration`, `renderComponent`, `getAllDisplayIssues`) are all undefined names. ' + - 'What would have to change: the block imports `describe`/`it`/`expect` from `vitest` and the three ' + - 'helpers from wherever this test suite ships them — or, if the helpers are suite-local and have no ' + - 'importable home, the block gets a `FRAGMENT_MARKER` saying so.', - 'packages/core/src/adapters/README.md': - '5 `ts` blocks (fences 36, 70, 191, 216, 239); 2 of them compile untouched. 3 fail with 9 diagnostics, ' + - 'ALL semantic-phase: TS2304 x2, TS2355 x6, TS2420 x1. Two are excerpts naming a value the prose ' + - 'introduces but the block never declares (`contextDataSource` at fence 191, `dataSource` at fence 216). ' + - "⚠️ The third is a DOCUMENTED-API defect rather than a snippet-hygiene one, and it is the first thing " + - 'this widening found: the custom-adapter template at fence 239 declares `class MyCustomAdapter ' + - "implements DataSource` while omitting `getObjectSchema`, which `DataSource` requires (TS2420), " + - 'and its six method bodies are `// Your implementation` comments under non-`void` return annotations ' + - '(TS2355 x6). A reader who copies it gets a class that does not satisfy the interface it claims. What ' + - 'would have to change: the template gains `getObjectSchema` and returns a value from each body (or ' + - 'declares the bodies elided), and the two excerpts declare the value they use.', - 'packages/types/src/zod/README.md': - '14 `ts` blocks (fences 88, 110, 140, 151, 220, 245, 266, 281, 289, 301, 324, 334, 354, 368); 5 compile ' + - 'untouched. 9 fail with 19 diagnostics: 3 fail in the syntax-phase (fences 220, 301, 368 — TS1109 x10, ' + - 'so their semantic half is UNMEASURED, not clean) and 6 fail in the semantic-phase (TS2304 x8, ' + - 'TS2307 x1). Three ' + - 'classes, each with its own remedy. (1) Fence 220 is a SHAPE SKETCH — a bare object literal at ' + - "statement position with `?:` optionality markers written on keys and `type: string` standing where a " + - 'value goes; its fence language should be one this gate does not compile. (2) Fences 301 and 368 write ' + - 'the elision `{ ... }` literally, which TypeScript reads as a spread with no operand. (3) The six ' + - 'semantic failures are EXCERPTS that continue an earlier block’s imports — `ButtonSchema`, ' + - '`CardSchema`, `userInput`, `internalConfig`, `useForm`, `registry` — plus one specifier no imported ' + - 'package declares (`@hookform/resolvers/zod`, TS2307), which is the bound this header states rather ' + - 'than a page defect. What would have to change: each excerpt made self-contained against the built ' + - '`dist/*.d.ts`, the two elisions written as real initialisers, and the sketch’s fence relabelled. ' + - '⭐ This page SHIPS: `packages/types` lists the whole of `src/` in its manifest `files`, so it is ' + - 'inside the npm tarball a reader downloads — which is why objectui#7308 filed it first.', + // objectui#7308's three nested-package-README rows were PAID DOWN by + // objectui#9412 and are gone from this object. Their record is in the header + // above, under "Batch 5"; nothing was softened here to retire them. // objectui#7856 card 2. Measured on `fedfa3e4` with this gate's own analyzer // against the closure `--build-filter` names (35/35 turbo tasks successful): // `analyze({ ungated: {} })` for the population, `compileSnippets()` for the