Scope Biome Grit plugins

This commit is contained in:
murashit 2026-06-25 23:45:50 +09:00
parent dd538d9e44
commit 7c5da3d520
10 changed files with 108 additions and 41 deletions

View file

@ -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": [

View file

@ -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")
}

View file

@ -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")

View file

@ -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")
}

View file

@ -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")
}
}

View file

@ -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")

View file

@ -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")
},

View file

@ -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")
}

View file

@ -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")

View file

@ -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, ""));
}