Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

Expand All @@ -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)

Expand Down
99 changes: 99 additions & 0 deletions app/src/lib/__tests__/docs-accuracy.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});