diff --git a/biome.jsonc b/biome.jsonc index f04e2c22..8e3f3b70 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -6,28 +6,89 @@ "useIgnoreFile": true }, "plugins": [ - // App-server and generated binding boundaries. - "./scripts/lint/no-app-server-projection-rpcs.grit", - "./scripts/lint/no-app-server-protocol-boundary-imports.grit", - "./scripts/lint/no-generated-app-server-boundary-imports.grit", - "./scripts/lint/no-generated-app-server-import-shapes.grit", - "./scripts/lint/no-lower-level-boundary-imports.grit", + // App-server protocol and generated binding boundaries. + { + "path": "./scripts/lint/no-app-server-projection-rpcs.grit", + "includes": ["**/src/features/chat/application/**/*.ts", "**/src/features/chat/application/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-app-server-protocol-boundary-imports.grit", + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx", "!**/src/app-server/**/*.ts", "!**/src/app-server/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-generated-app-server-boundary-imports.grit", + "includes": [ + "**/src/**/*.ts", + "**/src/**/*.tsx", + "!**/src/app-server/connection/**", + "!**/src/app-server/protocol/server-requests.ts" + ] + }, + + // Shared source layering boundaries. + { + "path": "./scripts/lint/no-generated-app-server-import-shapes.grit", + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-lower-level-boundary-imports.grit", + "includes": [ + "**/src/app-server/**/*.ts", + "**/src/app-server/**/*.tsx", + "**/src/domain/**/*.ts", + "**/src/domain/**/*.tsx", + "**/src/shared/**/*.ts", + "**/src/shared/**/*.tsx" + ] + }, // Chat feature architecture. - "./scripts/lint/no-chat-domain-outer-layer-imports.grit", - "./scripts/lint/no-chat-signal-imports.grit", - "./scripts/lint/no-implicit-dom-bridges.grit", - "./scripts/lint/no-pure-chat-state-side-effects.grit", - "./scripts/lint/no-ui-root-imports.grit", + { + "path": "./scripts/lint/no-chat-domain-outer-layer-imports.grit", + "includes": ["**/src/features/chat/domain/**/*.ts", "**/src/features/chat/domain/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-chat-signal-imports.grit", + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-pure-chat-state-side-effects.grit", + "includes": ["**/src/features/chat/application/state/**/*.ts", "**/src/features/chat/application/state/**/*.tsx"] + }, + + // UI and DOM source boundaries. + { + "path": "./scripts/lint/no-implicit-dom-bridges.grit", + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-ui-root-imports.grit", + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-uncontrolled-preact-form-state.grit", + "includes": ["**/src/**/*.tsx"] + }, // Project-wide source shape. - "./scripts/lint/no-handwritten-reexports.grit", - "./scripts/lint/no-self-referential-initializer-callback.grit", - "./scripts/lint/no-unsafe-iterator-value.grit", - "./scripts/lint/no-uncontrolled-preact-form-state.grit", + { + "path": "./scripts/lint/no-handwritten-reexports.grit", + "includes": ["**/*.ts", "**/*.tsx", "**/*.mjs"] + }, + { + "path": "./scripts/lint/no-self-referential-initializer-callback.grit", + "includes": ["**/*.ts", "**/*.tsx", "**/*.mjs"] + }, + { + "path": "./scripts/lint/no-unsafe-iterator-value.grit", + "includes": ["**/*.ts", "**/*.tsx", "**/*.mjs"] + }, // CSS source policy. - "./scripts/lint/no-restricted-css-policy.grit" + { + "path": "./scripts/lint/no-restricted-css-policy.grit", + "includes": ["**/src/styles/**/*.css"] + } ], "files": { "includes": [ diff --git a/scripts/lint/no-app-server-projection-rpcs.grit b/scripts/lint/no-app-server-projection-rpcs.grit index bf10b599..7a675961 100644 --- a/scripts/lint/no-app-server-projection-rpcs.grit +++ b/scripts/lint/no-app-server-projection-rpcs.grit @@ -7,6 +7,5 @@ or { `$client.rollbackThread($...)` as $stmt } where { $client <: r"^(?:client|[A-Za-z_$][A-Za-z0-9_$]*Client)$", - $filename <: r".*/src/features/chat/application/.*", register_diagnostic(span=$stmt, message="Keep app-server projection RPCs behind app-server facades; chat application code should consume Panel-owned snapshots or view models.", severity="error") } diff --git a/scripts/lint/no-app-server-protocol-boundary-imports.grit b/scripts/lint/no-app-server-protocol-boundary-imports.grit index f5a8337a..be663d67 100644 --- a/scripts/lint/no-app-server-protocol-boundary-imports.grit +++ b/scripts/lint/no-app-server-protocol-boundary-imports.grit @@ -17,8 +17,6 @@ or { }, or { { - $filename <: r".*/src/.*", - not { $filename <: r".*/src/app-server/.*" }, not { $filename <: r".*/src/features/chat/app-server/(?:inbound/notification-plan|mappers/message-stream/turn-items)\.ts$" }, not { $filename <: r".*/src/features/chat/app-server/inbound/(?:handler|routing)\.ts$" }, register_diagnostic(span=$stmt, message="Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.", severity="error") @@ -37,8 +35,6 @@ or { }, `require($source)` as $stmt where { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$", - $filename <: r".*/src/.*", - not { $filename <: r".*/src/app-server/.*" }, not { $filename <: r".*/src/features/chat/app-server/(?:inbound/notification-plan|mappers/message-stream/turn-items)\.ts$" }, not { $filename <: r".*/src/features/chat/app-server/inbound/(?:handler|routing)\.ts$" }, register_diagnostic(span=$stmt, message="Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.", severity="error") diff --git a/scripts/lint/no-chat-domain-outer-layer-imports.grit b/scripts/lint/no-chat-domain-outer-layer-imports.grit index bdc860b7..9958ed24 100644 --- a/scripts/lint/no-chat-domain-outer-layer-imports.grit +++ b/scripts/lint/no-chat-domain-outer-layer-imports.grit @@ -12,14 +12,12 @@ private pattern js_module_reference() { or { js_module_reference() as $stmt where { - $filename <: r".*/src/features/chat/domain/.*", $stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+|src/features/chat/)(?:app-server|application|host|panel|presentation|ui)(?:/.*)?[\"']$" }, register_diagnostic(span=$stmt, message="Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.", severity="error") }, `require($source)` as $stmt where { - $filename <: r".*/src/features/chat/domain/.*", $source <: r"^[\"'](?:(?:\.\./)+|src/features/chat/)(?:app-server|application|host|panel|presentation|ui)(?:/.*)?[\"']$", register_diagnostic(span=$stmt, message="Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.", severity="error") } diff --git a/scripts/lint/no-generated-app-server-boundary-imports.grit b/scripts/lint/no-generated-app-server-boundary-imports.grit index c04d06ec..08b70a4d 100644 --- a/scripts/lint/no-generated-app-server-boundary-imports.grit +++ b/scripts/lint/no-generated-app-server-boundary-imports.grit @@ -13,14 +13,10 @@ private pattern js_module_reference() { or { js_module_reference() as $stmt where { $stmt <: contains `$source` where { $source <: r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$" }, - not { $filename <: r".*/(?:src/generated|src/app-server/connection|tests/app-server)/.*" }, - not { $filename <: r".*/src/app-server/protocol/server-requests\.ts$" }, - register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", severity="error") + register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", severity="error") }, `require($source)` as $stmt where { $source <: r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$", - not { $filename <: r".*/(?:src/generated|src/app-server/connection|tests/app-server)/.*" }, - not { $filename <: r".*/src/app-server/protocol/server-requests\.ts$" }, - register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", severity="error") + register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", severity="error") } } diff --git a/scripts/lint/no-implicit-dom-bridges.grit b/scripts/lint/no-implicit-dom-bridges.grit index ae302334..14fc3fbb 100644 --- a/scripts/lint/no-implicit-dom-bridges.grit +++ b/scripts/lint/no-implicit-dom-bridges.grit @@ -13,7 +13,6 @@ or { `$target.addEventListener($...)` as $stmt, `$target.removeEventListener($...)` as $stmt } where { - $filename <: r".*/src/.*\.tsx?$", not { $filename <: r".*\.(?:dom|obsidian|measure)\.tsx?$" }, not { $target <: `signal` }, register_diagnostic(span=$stmt, message="Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.", severity="error") diff --git a/scripts/lint/no-lower-level-boundary-imports.grit b/scripts/lint/no-lower-level-boundary-imports.grit index 5dd786d4..f721d2b0 100644 --- a/scripts/lint/no-lower-level-boundary-imports.grit +++ b/scripts/lint/no-lower-level-boundary-imports.grit @@ -22,7 +22,6 @@ or { register_diagnostic(span=$stmt, message="Domain modules must stay pure and generated-independent; keep app-server, settings, workspace, UI, and Obsidian dependencies at boundary callers.", severity="error") }, js_module_reference() as $stmt where { - $filename <: r".*/src/(?:app-server|domain|shared)/.*", $stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+features|src/features)(?:/.*)?[\"']$" }, register_diagnostic(span=$stmt, message="Lower-level modules must not import feature modules or app-server connection internals. Move shared behavior to shared, domain, or app-server adapters.", severity="error") }, @@ -42,7 +41,6 @@ or { register_diagnostic(span=$stmt, message="Domain modules must stay pure and generated-independent; keep app-server, settings, workspace, UI, and Obsidian dependencies at boundary callers.", severity="error") }, `require($source)` as $stmt where { - $filename <: r".*/src/(?:app-server|domain|shared)/.*", $source <: r"^[\"'](?:(?:\.\./)+features|src/features)(?:/.*)?[\"']$", register_diagnostic(span=$stmt, message="Lower-level modules must not import feature modules or app-server connection internals. Move shared behavior to shared, domain, or app-server adapters.", severity="error") }, diff --git a/scripts/lint/no-pure-chat-state-side-effects.grit b/scripts/lint/no-pure-chat-state-side-effects.grit index 06af9dd0..fe87d7b9 100644 --- a/scripts/lint/no-pure-chat-state-side-effects.grit +++ b/scripts/lint/no-pure-chat-state-side-effects.grit @@ -14,6 +14,5 @@ or { `localStorage.$property` as $stmt, `sessionStorage.$property` as $stmt } where { - $filename <: r".*/src/features/chat/application/state/.*", register_diagnostic(span=$stmt, message="Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", severity="error") } diff --git a/scripts/lint/no-ui-root-imports.grit b/scripts/lint/no-ui-root-imports.grit index 7f7b0236..28150871 100644 --- a/scripts/lint/no-ui-root-imports.grit +++ b/scripts/lint/no-ui-root-imports.grit @@ -12,13 +12,11 @@ private pattern js_module_reference() { or { js_module_reference() as $stmt where { - $filename <: r".*/src/.*", $stmt <: contains `$source` where { $source <: r"^[\"'].*shared/ui/ui-root\.dom[\"']$" }, not { $filename <: r".*\.(?:dom|obsidian|measure)\.tsx?$" }, register_diagnostic(span=$stmt, message="Import the Preact root adapter only from explicit root bridge files.", severity="error") }, `require($source)` as $stmt where { - $filename <: r".*/src/.*", $source <: r"^[\"'].*shared/ui/ui-root\.dom[\"']$", not { $filename <: r".*\.(?:dom|obsidian|measure)\.tsx?$" }, register_diagnostic(span=$stmt, message="Import the Preact root adapter only from explicit root bridge files.", severity="error") diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index 5c059061..39fa3faf 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -1,4 +1,5 @@ import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -7,6 +8,12 @@ import { describe, expect, it } from "vitest"; const repoRoot = process.cwd(); const biomeBin = path.join(repoRoot, "node_modules", ".bin", "biome"); const workspaceByPlugins = new Map(); +const projectPluginByName = new Map( + parseJsonc(readFileSync(path.join(repoRoot, "biome.jsonc"), "utf8")).plugins.map((plugin) => { + const pluginPath = typeof plugin === "string" ? plugin : plugin.path; + return [path.basename(pluginPath), plugin]; + }), +); describe("GritQL source policy", () => { it("keeps project-wide source-shape policies enforceable as Biome plugin diagnostics", async () => { @@ -410,21 +417,21 @@ export type Request = ServerRequest; ); expect(pluginMessages(report, "src/features/chat/domain/generated-thread.ts")).toEqual([ - "Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", + "Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", ]); expect(pluginMessages(report, "src/features/chat/domain/generated-thread-import.ts")).toEqual([ - "Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", + "Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", ]); expect(pluginDiagnostics(report, "src/app-server/connection/generated-thread.ts")).toEqual([]); expect(pluginDiagnostics(report, "tests/app-server/generated-thread.test.ts")).toEqual([]); expect(pluginMessages(report, "src/app-server/protocol/request-input.ts")).toEqual([ - "Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", + "Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", ]); expect(pluginMessages(report, "src/app-server/services/runtime-overrides.ts")).toEqual([ - "Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", + "Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", ]); expect(pluginMessages(report, "src/app-server/protocol/turn.ts")).toEqual([ - "Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.", + "Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", ]); expect(pluginDiagnostics(report, "src/app-server/protocol/server-requests.ts")).toEqual([]); }); @@ -836,7 +843,7 @@ async function tempBiomeWorkspace(plugins) { JSON.stringify({ $schema: "https://biomejs.dev/schemas/2.5.1/schema.json", vcs: { enabled: false }, - plugins: plugins.map((plugin) => path.join(repoRoot, "scripts", "lint", plugin)), + plugins: plugins.map((plugin) => projectPluginConfig(plugin)), css: { linter: { enabled: true } }, }), ); @@ -861,6 +868,18 @@ function biomeLint(files, cwd, options = {}) { return report; } +function projectPluginConfig(plugin) { + const projectPlugin = projectPluginByName.get(plugin); + if (!projectPlugin) { + throw new Error(`Missing ${plugin} in biome.jsonc plugins`); + } + const pluginPath = path.join(repoRoot, "scripts", "lint", plugin); + if (typeof projectPlugin === "string") { + return pluginPath; + } + return { ...projectPlugin, path: pluginPath }; +} + function pluginDiagnostics(report, filePath) { return report.diagnostics .filter((diagnostic) => diagnostic.category === "plugin" && diagnosticMatchesFile(report, diagnostic, filePath)) @@ -900,3 +919,7 @@ function parseBiomeJsonReport(stdout, stderr) { } return JSON.parse(stdout.slice(jsonStart)); } + +function parseJsonc(source) { + return JSON.parse(source.replace(/^\s*\/\/.*$/gm, "")); +}