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.", +);