diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 74947a43..40504614 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -197,9 +197,9 @@ app/src/ └── proxy.ts # Edge middleware (auth guard) component/src/ -├── charts/ # ECharts wrappers (BaseChart + 12 types) +├── charts/ # ECharts wrappers (BaseChart + 14 types) ├── components/ -│ ├── ui/ # 33 shadcn/ui primitives +│ ├── ui/ # 38 shadcn/ui primitives │ └── composed/ # 42 higher-order components ├── hooks/ # useWidgetSize, useContainerSize └── lib/ # Utilities, design tokens, Cypher language diff --git a/CLAUDE.md b/CLAUDE.md index 86cce60d..888e2f0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ Rules: | Layer | Tool | Examples | | -------------------- | ----------------------- | -------------------------------------------------------------------------------- | -| Pure functions/utils | Vitest (no DOM) | chart-registry, normalize-value, date-utils, query-hash, wrap-with-preview-limit | +| Pure functions/utils | Vitest (no DOM) | chart-plugin-registry, normalize-value, date-utils, query-hash, wrap-with-preview-limit | | API routes | Vitest (mocked DB/auth) | Validation, permissions, error handling | | Zustand stores | Vitest (no mocks) | State transitions, cascading logic | | Store orchestration | Vitest (no DOM) | parameter-widget-renderer interactions, type coercion | @@ -125,7 +125,8 @@ Playwright E2E with **server-side coverage collection** (`collectServer: true` i ## Multi-Tenancy -- `tenant_id` column on ALL tables. Every DB query MUST include tenant filter at ORM/middleware level. +- `tenant_id` column on ALL tables. Every DB query MUST include an explicit tenant filter — `eq(table.tenantId, session.tenantId)` — written **per query, in the route**. There is no ORM-level or middleware-level enforcement today (`app/src/lib/db/index.ts` is a plain Drizzle client), so a forgotten filter is a cross-tenant leak that nothing catches. Adding a guard is tracked in #1226. +- Take `tenantId` from `requireSession()`, NEVER from the request body. - JWT tokens include `tenantId` claim. Validate before ANY DB or API access. - SaaS vs on-prem: env vars only, never code branches. @@ -144,7 +145,7 @@ Includes: SSO, Custom Roles, Connector Labels, Bulk Import, Connector CRUD API, ## Migrations Forward-only. Idempotent. Advisory lock prevents concurrent runs. -Test version-skip paths. `--skip-migrations` flag exists for emergency debugging. +Test version-skip paths. Boot migrations are controlled by `MIGRATE_ON_START` (`1`/`true` to run; set `0` to skip for emergency debugging) — there is no `--skip-migrations` CLI flag. ## Automated Guardrails (Hooks) diff --git a/app/src/lib/__tests__/docs-accuracy.test.ts b/app/src/lib/__tests__/docs-accuracy.test.ts new file mode 100644 index 00000000..d19e307e --- /dev/null +++ b/app/src/lib/__tests__/docs-accuracy.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * Guards the repo's own documentation against drift (#1235). + * + * CLAUDE.md is loaded as ground truth by agent sessions, so a stale path or + * count there becomes a wrong assumption in generated code. These tests fail + * loudly instead. + */ + +const REPO_ROOT = resolve( + dirname(fileURLToPath(import.meta.url)), + "../../../..", +); + +const readDoc = (name: string) => + readFileSync(resolve(REPO_ROOT, name), "utf8"); + +/** Top-level dirs that make a backticked token a repo path rather than an npm specifier. */ +const REPO_PREFIXES = [ + "app/", + "component/", + "connection/", + "connector-sdk/", + "cli/", + "docs/", + "scripts/", + ".claude/", + ".github/", + "docker/", +]; + +function referencedPaths(markdown: string): string[] { + const backticked = markdown.match(/`[^`\s]+`/g) ?? []; + return [ + ...new Set( + backticked + .map((t) => t.slice(1, -1)) + .filter((t) => REPO_PREFIXES.some((p) => t.startsWith(p))), + ), + ]; +} + +const countFiles = (dir: string, ext = ".tsx") => + readdirSync(resolve(REPO_ROOT, dir)).filter((f) => f.endsWith(ext)).length; + +describe("documentation accuracy", () => { + describe.each(["CLAUDE.md", "ARCHITECTURE.md"])("%s", (docName) => { + it("references only file paths that exist", () => { + const missing = referencedPaths(readDoc(docName)).filter( + (p) => !existsSync(resolve(REPO_ROOT, p)), + ); + expect(missing).toEqual([]); + }); + }); + + describe("ARCHITECTURE.md component counts match the filesystem", () => { + // Each regex must match: a reworded claim should fail here rather than + // silently stop being checked. + it.each([ + [ + "shadcn/ui primitives", + /(\d+) shadcn\/ui primitives/, + () => countFiles("component/src/components/ui"), + ], + [ + "composed components", + /(\d+) higher-order components/, + () => countFiles("component/src/components/composed"), + ], + [ + "chart modules", + /BaseChart \+ (\d+) types/, + // charts/ holds base-chart.tsx plus one module per chart type. + () => countFiles("component/src/charts") - 1, + ], + ])("%s", (_label, pattern, actual) => { + const match = readDoc("ARCHITECTURE.md").match(pattern); + expect( + match, + `claim matching ${pattern} not found — was it reworded?`, + ).not.toBeNull(); + expect(Number(match![1])).toBe(actual()); + }); + }); + + it("CLAUDE.md documents MIGRATE_ON_START, not a --skip-migrations flag", () => { + // Naming the flag to debunk it is fine (readers search for it); asserting + // it exists is not. The real escape hatch is MIGRATE_ON_START=0 (#1222). + const doc = readDoc("CLAUDE.md"); + expect(doc).toContain("MIGRATE_ON_START"); + // Assert the canonical debunk is present rather than blocklisting one + // phrasing — "use `--skip-migrations`" would slip past a negative regex. + expect(doc).toContain("there is no `--skip-migrations` CLI flag"); + }); +});