From 8107f7a66dee711825d47bdea6e212c329aafeb2 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Tue, 28 Jul 2026 19:06:00 +1000 Subject: [PATCH] docs: generate the stack-supabase reference, and stop overstating the API gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes, all about the docs telling the truth about the SDK. 1. Add @cipherstash/stack-supabase to the TypeDoc entry points. Stack 1.0 split the Supabase adapter into its own package, and the entry points were never updated, so NOTHING in it reached the generated reference — encryptedSupabaseV3, the query builder, and the EQL-version constraints on its encrypted operators were all absent. 19 pages now generate (763 total, up from 706), including the constraint that free-text and JSON operators are "unavailable with EQL 3.0.2+" because PostgREST cannot express the required eql_v3.query_* cast. That is now published automatically instead of relying on someone reading the source. 2. Resolve a workspace package's subpath imports to source. stack-supabase imports its sibling by package name (@cipherstash/stack, /schema, /adapter-kit, ...), which resolves through that package's exports map to ./dist — and the clone is never built, so all 42 imports were TS2307 and every cross-package type would have documented as `any`. Derive the tsconfig `paths` from the sibling's own exports map, rewriting ./dist/x.js to ./packages//src/x.ts, rather than hardcoding twelve subpaths that would rot the next time Stack adds one. Two of them are not guessable (`/types` -> types-public.ts, `/v3` -> encryption/v3.ts), which is the argument for deriving rather than transcribing. 3. Say what the content API gate actually checks. It printed "no deprecated or non-existent API found in documentation" while the Supabase page taught `.contains()` for encrypted free-text, which raises. It is a denylist of 7 patterns, not verification, so it now reports the rule count and files scanned and states the limitation outright. Deliberately NOT adding the version-diff denylist discussed: `contains()` was repurposed rather than removed, so it is present in every tag and no symbol-presence or signature diff fires on it. Catching that class needs the examples typechecked against the SDK; the header now records that. Also removes CLAUDE.md's "Required Skills" section. All six skills it mandated (encryption, secrets, drizzle, supabase, dynamodb, update-docs) do not exist, and its validation checklist was stale too — `switcher` appears in 0 files and the "Good to know" callout in 1, versus in 37. Its accurate items are already covered by the Frontmatter, Code Blocks, and Formatting sections above. Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8 --- CLAUDE.md | 43 ---------------------------- scripts/generate-docs.ts | 16 +++++++++-- scripts/lib/docs-generator.ts | 50 +++++++++++++++++++++++++++++++++ scripts/validate-content-api.ts | 43 +++++++++++++++++++++++----- 4 files changed, 99 insertions(+), 53 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c827c47..761d148 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,46 +102,3 @@ Fumadocs UI theme with custom purple primary color (`hsl(269, 70%, 45%)`). Dark ## Formatting Biome handles both linting and formatting. 2-space indentation. Biome organizes imports automatically. Run `bun run format` before committing. - -## Required Skills - -Always use the following agent skills when working in this repository. These skills contain the canonical API references and must be consulted to ensure documentation accuracy. - -### CipherStash Product Skills - -Use these skills whenever writing or editing documentation about the corresponding product area. They contain the complete, up-to-date API surface, code examples, and type signatures. - -| Skill | Use when editing docs in | -|---|---| -| `encryption` | `content/docs/encryption/` — Schema definition, encrypt/decrypt, searchable encryption, bulk operations, identity-aware encryption, error handling, migration | -| `secrets` | `content/docs/secrets/` — Secrets SDK (`set`/`get`/`getMany`/`list`/`delete`), `stash` CLI usage, environment isolation | -| `drizzle` | `content/docs/encryption/drizzle.mdx` — `encryptedType` column, `extractEncryptionSchema`, `createEncryptionOperators`, encrypted query operators, batched and/or, EQL migrations | -| `supabase` | `content/docs/encryption/supabase.mdx` — `encryptedSupabase` wrapper, transparent encrypt/decrypt on insert/update/select, query filters (eq, like, gt, in, or, match), identity-aware encryption | -| `dynamodb` | `content/docs/encryption/dynamodb.mdx` — `encryptedDynamoDB` helper, `__source`/`__hmac` attribute naming, encrypted partition/sort keys, bulk operations, audit logging | - -### Documentation Update Skill - -Use the `update-docs` skill whenever: -- Creating new documentation pages -- Updating existing documentation based on code changes -- Reviewing documentation completeness for a PR -- Scaffolding docs for a new feature - -The `update-docs` skill enforces the following workflow: - -1. **Analyze changes** — Diff the branch to identify affected files -2. **Map to docs** — Use the code-to-docs mapping to find which MDX files need updates -3. **Review each doc** — Walk through updates with user confirmation before editing -4. **Validate** — Run `bun run lint` to check formatting -5. **Commit** — Stage documentation changes - -#### Documentation Validation Checklist - -Before committing documentation changes, verify: - -- Frontmatter has `title` and `description` -- Code blocks have `filename` attribute -- TypeScript examples come first, with `switcher` and a JS variant where appropriate -- Props/options tables are properly formatted -- Notes use the `> **Good to know**:` callout pattern -- `bun run lint` passes diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 3ba8391..697624c 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -19,8 +19,14 @@ const stackConfig: DocsConfig = { baseOutputDir: path.join(process.cwd(), "content/stack/reference/stack"), // Stack 1.0 split the Drizzle and Supabase adapters into their own packages // (@cipherstash/stack-drizzle, @cipherstash/stack-supabase) and removed the - // secrets module, so those three entry points no longer exist under - // packages/stack/src. Keep only the modules @cipherstash/stack still ships. + // secrets module, so those entry points no longer exist under + // packages/stack/src. + // + // `stack-supabase` is documented from its new home. It is a separate npm + // package but the same reference surface to a reader, and without it nothing + // in the Supabase adapter — `encryptedSupabaseV3`, the query builder, the + // EQL-version constraints on its encrypted operators — reaches the generated + // reference at all. entryPoints: [ "./packages/stack/src/encryption/index.ts", "./packages/stack/src/schema/index.ts", @@ -29,8 +35,12 @@ const stackConfig: DocsConfig = { "./packages/stack/src/types-public.ts", "./packages/stack/src/client.ts", "./packages/stack/src/errors/index.ts", + "./packages/stack-supabase/src/index.ts", + ], + tsconfigInclude: [ + "packages/stack/src/**/*", + "packages/stack-supabase/src/**/*", ], - tsconfigInclude: ["packages/stack/src/**/*"], tagFilter: (tag: string) => tag.includes("@cipherstash/stack@") && !tag.includes("stack-"), referencePathSegment: "stack", diff --git a/scripts/lib/docs-generator.ts b/scripts/lib/docs-generator.ts index e5c7579..92cc52b 100644 --- a/scripts/lib/docs-generator.ts +++ b/scripts/lib/docs-generator.ts @@ -160,6 +160,55 @@ export function getVersionsToGenerate( }); } +/** + * TypeScript `paths` that resolve a workspace package to its SOURCE. + * + * An adapter package (`stack-supabase`) imports its sibling by package name — + * `@cipherstash/stack`, `@cipherstash/stack/schema`, `.../adapter-kit` — which + * resolves through that package's `exports` map to `./dist/*`. The clone is + * never built, so there is no `dist` and every such import is TS2307. TypeDoc + * then documents the adapter with all its cross-package types missing. + * + * Derive the mapping from the sibling's own `exports` map rather than hardcoding + * it: rewrite each subpath's `./dist/x.js` target to `./packages//src/x.ts`. + * A hand-written list would silently rot the next time Stack adds a subpath, and + * the failure mode is exactly the one this is fixing — a missing type quietly + * becoming `any` in the published reference. + */ +async function workspaceSourcePaths( + workingDir: string, + packageName: string, +): Promise> { + const dir = packageName.split("/").pop(); + const manifest = path.join(workingDir, "packages", dir ?? "", "package.json"); + + let exportsMap: Record; + try { + const pkg = JSON.parse(await fs.readFile(manifest, "utf8")); + exportsMap = pkg.exports ?? {}; + } catch { + // The layout moved. Better to emit nothing and let TS2307 name the missing + // module than to invent paths that point at files which do not exist. + console.warn(` ! no exports map at ${manifest}; skipping source paths`); + return {}; + } + + const paths: Record = {}; + for (const [subpath, target] of Object.entries(exportsMap)) { + if (subpath.endsWith("package.json")) continue; + // Any condition will do — every branch points at the same module, and only + // the path shape matters here. + const dist = JSON.stringify(target).match(/\.\/dist\/[^"]+\.js/)?.[0]; + if (!dist) continue; + + const stem = dist.replace(/^\.\/dist\//, "").replace(/\.js$/, ""); + const specifier = + subpath === "." ? packageName : `${packageName}/${subpath.slice(2)}`; + paths[specifier] = [`./packages/${dir}/src/${stem}.ts`]; + } + return paths; +} + /** * Generate documentation for a specific tag */ @@ -229,6 +278,7 @@ export async function generateDocsForTag( "@/*": ["./packages/stack/src/*"], "@cipherstash/schema": ["./packages/schema/src/index.ts"], "@cipherstash/schema/*": ["./packages/schema/src/*"], + ...(await workspaceSourcePaths(workingDir, "@cipherstash/stack")), }, }, }; diff --git a/scripts/validate-content-api.ts b/scripts/validate-content-api.ts index 30d0b61..c6c0df2 100644 --- a/scripts/validate-content-api.ts +++ b/scripts/validate-content-api.ts @@ -2,11 +2,22 @@ /** * Content API gate. * - * Fails the build when documentation teaches an API that is deprecated, was - * removed, or never existed. Two long-lived content branches plus a fast-moving - * SDK means a page fixed on one branch can be silently reintroduced by a port - * from the other — which is exactly how the deprecated `LockContext.identify()` - * flow survived a fix and came back in a comparison-page port. + * Fails the build when documentation teaches one of the specific APIs listed in + * RULES below. Two long-lived content branches plus a fast-moving SDK means a + * page fixed on one branch can be silently reintroduced by a port from the + * other — which is exactly how the deprecated `LockContext.identify()` flow + * survived a fix and came back in a comparison-page port. + * + * ── What this does NOT do ────────────────────────────────────────────────── + * This is a DENYLIST, not verification. It catches only what someone thought to + * add, and cannot see an API that changed MEANING rather than name. Stack's + * `contains()` → `matches()` rename is the worked example: `contains()` still + * exists, so nothing here (or in any symbol-presence diff) fires, yet calling it + * on an encrypted text column now raises. That shipped in the docs. + * + * Catching that class needs the examples themselves typechecked against the + * installed SDK, the way Rust doctests are compiled. Until then, treat a pass + * as "none of the known-bad patterns appeared", which is what it prints. * * Run via `bun run validate-content`; wired into prebuild. * @@ -138,11 +149,17 @@ function inScope(relative: string, rule: Rule): boolean { const root = process.cwd(); const findings: Finding[] = []; +let filesScanned = 0; +let filesSkipped = 0; for (const dir of ["content/docs", "content/stack"]) { for (const file of collectFiles(path.join(root, dir))) { const relative = path.relative(root, file).split(path.sep).join("/"); - if (isSkipped(relative)) continue; + if (isSkipped(relative)) { + filesSkipped++; + continue; + } + filesScanned++; const lines = fs.readFileSync(file, "utf8").split("\n"); lines.forEach((text, i) => { @@ -192,4 +209,16 @@ if (findings.length > 0) { process.exit(1); } -console.log("✓ no deprecated or non-existent API found in documentation"); +// Say what was actually checked. This gate is a DENYLIST: it can only catch the +// APIs listed in RULES, so an unqualified "no non-existent API found" overstates +// it — and did, while `content/docs/reference/stack/supabase.mdx` taught +// `.contains()` for encrypted free-text, which raises. Reporting the rule count +// makes the coverage legible at a glance, so the next reader can tell the +// difference between "verified" and "matched nothing on a short list". +console.log( + `✓ content API gate: no occurrences of ${RULES.length} known-retired API pattern(s) ` + + `in ${filesScanned} file(s) (${filesSkipped} generated/pinned file(s) skipped).`, +); +console.log( + " Denylist only: it does not verify that the APIs the docs DO name still exist.", +);