diff --git a/biome.jsonc b/biome.jsonc index 4d94c68f..c6ce4419 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -13,16 +13,7 @@ }, { "path": "./scripts/lint/no-app-server-protocol-boundary-imports.grit", - "includes": [ - "**/src/**/*.ts", - "!**/src/app-server/**/*.ts", - "**/src/features/chat/panel/**/*.tsx", - "**/src/features/chat/ui/**/*.tsx", - "**/src/features/selection-rewrite/**/*.tsx", - "**/src/features/threads-view/**/*.tsx", - "**/src/settings/**/*.tsx", - "**/src/shared/ui/**/*.tsx" - ] + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx", "!**/src/app-server/**"] }, { "path": "./scripts/lint/no-generated-app-server-boundary-imports.grit", @@ -50,11 +41,11 @@ // Chat feature architecture. { - "path": "./scripts/lint/no-chat-signal-imports.grit", - "includes": ["**/src/**/*.ts", "**/src/**/*.tsx"] + "path": "./scripts/lint/no-preact-signal-imports.grit", + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx", "!**/src/features/chat/panel/shell-state.tsx"] }, { - "path": "./scripts/lint/no-pure-chat-state-side-effects.grit", + "path": "./scripts/lint/no-state-module-side-effects.grit", "includes": ["**/src/features/chat/application/state/**/*.ts"] }, diff --git a/docs/development.md b/docs/development.md index bb18a8e8..de9cf829 100644 --- a/docs/development.md +++ b/docs/development.md @@ -14,7 +14,7 @@ Use this as the normal edit loop. `npm run fix` applies Biome formatting, import Use focused scripts for tight loops: `npm run typecheck`, `npm run test`, `npm run build`, or the targeted `npm run check:*` scripts. CI and release preflight run the same `npm run check` command as local development. -Biome owns formatting, import organization, general JavaScript/TypeScript/JSON/CSS linting, accessibility linting, Preact-compatible React domain rules, GritQL source-shape plugins, GritQL import-boundary restrictions, imperative-DOM bridge naming, and GritQL CSS source policy. Biome warnings fail `npm run check`; rule suppressions must stay local and include the Obsidian-specific rationale when a native Obsidian UI pattern intentionally diverges from a generic browser semantic. The CSS usage script still checks dead authored classes. TypeScript provides compiler-backed checks such as strict type checking, unused locals and parameters, implicit return coverage, switch fallthrough prevention, and deep-readonly `ChatState` snapshots. ESLint runs the TypeScript ESLint strict type-checked preset over source files, plus Obsidian plugin policy for `manifest.json` and `LICENSE`. Tests are covered by TypeScript, Vitest, and Biome rather than typed ESLint so local preflight stays responsive. Some checks intentionally overlap between Biome, TypeScript, and ESLint so the configuration stays simple. +`npm run check` combines Biome, TypeScript, Vitest, ESLint, CSS usage checks, and the production build. Biome warnings fail the check. Keep rule suppressions local and include the Obsidian-specific reason when a native Obsidian UI pattern intentionally diverges from a generic browser rule. ## Generated and Loaded Files diff --git a/scripts/lint/no-app-server-connection-boundary-imports.grit b/scripts/lint/no-app-server-connection-boundary-imports.grit index 79b8f5a1..8feac14a 100644 --- a/scripts/lint/no-app-server-connection-boundary-imports.grit +++ b/scripts/lint/no-app-server-connection-boundary-imports.grit @@ -28,5 +28,5 @@ private pattern app_server_connection_boundary_source() { js_module_reference() as $stmt where { $stmt <: contains `$source` where { $source <: app_server_connection_boundary_source() }, - register_diagnostic(span=$stmt, message="App-server protocol, domain, and shared modules must not import app-server connection internals. Keep connection usage at app-server adapters.", severity="error") + register_diagnostic(span=$stmt, message="Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", severity="error") } diff --git a/scripts/lint/no-app-server-projection-rpcs.grit b/scripts/lint/no-app-server-projection-rpcs.grit index 7a675961..15461edb 100644 --- a/scripts/lint/no-app-server-projection-rpcs.grit +++ b/scripts/lint/no-app-server-projection-rpcs.grit @@ -7,5 +7,5 @@ or { `$client.rollbackThread($...)` as $stmt } where { $client <: r"^(?:client|[A-Za-z_$][A-Za-z0-9_$]*Client)$", - 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") + register_diagnostic(span=$stmt, message="Keep app-server projection RPCs behind app-server facades; consume Panel-owned snapshots or view models here.", 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 e31037d1..d67676c8 100644 --- a/scripts/lint/no-app-server-protocol-boundary-imports.grit +++ b/scripts/lint/no-app-server-protocol-boundary-imports.grit @@ -22,5 +22,5 @@ js_module_reference() as $stmt where { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol/server-requests|src/app-server/protocol/server-requests)(?:/.*)?[\"']$" } }, - 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 turn-item conversion may consume turn protocol, and chat request handling may consume server request protocol at their app-server boundaries; feature state and UI must use Panel-owned models.", severity="error") + register_diagnostic(span=$stmt, message="Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol, and chat request handling may consume server request protocol at their app-server boundaries; feature state and UI must use Panel-owned models.", severity="error") } diff --git a/scripts/lint/no-lower-level-feature-imports.grit b/scripts/lint/no-lower-level-feature-imports.grit index 3e35daaf..008f7125 100644 --- a/scripts/lint/no-lower-level-feature-imports.grit +++ b/scripts/lint/no-lower-level-feature-imports.grit @@ -12,5 +12,5 @@ private pattern js_module_reference() { js_module_reference() as $stmt where { $stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+features|src/features)(?:/.*)?[\"']$" }, - register_diagnostic(span=$stmt, message="Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server adapters.", severity="error") + register_diagnostic(span=$stmt, message="Do not import feature modules from this layer. Move shared behavior to shared, domain, or app-server adapters.", severity="error") } diff --git a/scripts/lint/no-misplaced-tsx.grit b/scripts/lint/no-misplaced-tsx.grit index e317bbf0..612b3c79 100644 --- a/scripts/lint/no-misplaced-tsx.grit +++ b/scripts/lint/no-misplaced-tsx.grit @@ -1,5 +1,5 @@ language js `$program` where { - register_diagnostic(span=$program, message="Keep TSX files in UI, panel, settings, or explicit render bridge modules; non-rendering source should use .ts.", severity="error") + register_diagnostic(span=$program, message="Keep TSX files in rendering-owned source folders; non-rendering source should use .ts.", severity="error") } diff --git a/scripts/lint/no-chat-signal-imports.grit b/scripts/lint/no-preact-signal-imports.grit similarity index 55% rename from scripts/lint/no-chat-signal-imports.grit rename to scripts/lint/no-preact-signal-imports.grit index 174a00d1..79ba7036 100644 --- a/scripts/lint/no-chat-signal-imports.grit +++ b/scripts/lint/no-preact-signal-imports.grit @@ -12,6 +12,5 @@ private pattern js_module_reference() { js_module_reference() as $stmt where { $stmt <: contains `$source` where { $source <: r"^[\"']@preact/signals[\"']$" }, - not { $filename <: r".*/src/features/chat/panel/shell-state\.tsx$" }, - register_diagnostic(span=$stmt, message="Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.", severity="error") + register_diagnostic(span=$stmt, message="Do not import @preact/signals from this module.", severity="error") } diff --git a/scripts/lint/no-pure-chat-state-side-effects.grit b/scripts/lint/no-state-module-side-effects.grit similarity index 71% rename from scripts/lint/no-pure-chat-state-side-effects.grit rename to scripts/lint/no-state-module-side-effects.grit index fe87d7b9..10100a6d 100644 --- a/scripts/lint/no-pure-chat-state-side-effects.grit +++ b/scripts/lint/no-state-module-side-effects.grit @@ -14,5 +14,5 @@ or { `localStorage.$property` as $stmt, `sessionStorage.$property` as $stmt } where { - register_diagnostic(span=$stmt, message="Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", severity="error") + register_diagnostic(span=$stmt, message="Keep this state module deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", severity="error") } diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index 9f399d55..f4d5f589 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -15,7 +15,7 @@ const projectPluginByName = new Map( }), ); const APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE = - "Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol, and chat request handling may consume server request protocol at their app-server boundaries; feature state and UI must use Panel-owned models."; + "Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol, and chat request handling may consume server request protocol at their app-server boundaries; feature state and UI must use Panel-owned models."; describe("GritQL source policy", () => { it("keeps project-wide source-shape policies enforceable as Biome plugin diagnostics", async () => { @@ -134,9 +134,9 @@ runner = new Runner(() => runner.stop()); it("keeps chat architecture policies behind their intended boundaries", async () => { const cwd = await tempBiomeWorkspace([ - "no-chat-signal-imports.grit", "no-implicit-dom-bridges.grit", - "no-pure-chat-state-side-effects.grit", + "no-preact-signal-imports.grit", + "no-state-module-side-effects.grit", "no-ui-root-imports.grit", ]); await writeFile( @@ -262,14 +262,12 @@ export function timestamp(): number { ); expect(pluginDiagnostics(report, "src/features/chat/panel/shell-state.tsx")).toEqual([]); - expect(pluginMessages(report, "src/shared/ui/components.tsx")).toEqual([ - "Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.", - ]); + expect(pluginMessages(report, "src/shared/ui/components.tsx")).toEqual(["Do not import @preact/signals from this module."]); expect(pluginMessages(report, "src/shared/ui/signal-escapes.tsx")).toEqual([ - "Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.", - "Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.", - "Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.", - "Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.", + "Do not import @preact/signals from this module.", + "Do not import @preact/signals from this module.", + "Do not import @preact/signals from this module.", + "Do not import @preact/signals from this module.", ]); expect(pluginMessages(report, "src/features/chat/ui/dom-bridge-escape.tsx")).toEqual([ "Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.", @@ -288,8 +286,8 @@ export function timestamp(): number { "Import the Preact root adapter only from explicit root bridge files.", ]); expect(pluginMessages(report, "src/features/chat/application/state/message-stream.ts")).toEqual([ - "Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", - "Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", + "Keep this state module deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", + "Keep this state module deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", ]); expect(pluginDiagnostics(report, "src/features/chat/application/threads/resume-actions.ts")).toEqual([]); }); @@ -333,7 +331,7 @@ export type Input = UserInput; await writeFile( path.join(cwd, "src/app-server/services/runtime-overrides.ts"), ` -import type { ModelListResponse } from "../generated/app-server/v2/ModelListResponse"; +import type { ModelListResponse } from "../../generated/app-server/v2/ModelListResponse"; export interface RuntimeOverrideModelClient { listModels(includeHidden: boolean): Promise; @@ -438,10 +436,10 @@ export const value =
;
     );
 
     expect(pluginMessages(report, "src/features/chat/application/state/view.tsx")).toEqual([
-      "Keep TSX files in UI, panel, settings, or explicit render bridge modules; non-rendering source should use .ts.",
+      "Keep TSX files in rendering-owned source folders; non-rendering source should use .ts.",
     ]);
     expect(pluginMessages(report, "src/domain/threads/view.tsx")).toEqual([
-      "Keep TSX files in UI, panel, settings, or explicit render bridge modules; non-rendering source should use .ts.",
+      "Keep TSX files in rendering-owned source folders; non-rendering source should use .ts.",
     ]);
     expect(pluginDiagnostics(report, "src/features/chat/ui/message.tsx")).toEqual([]);
     expect(pluginDiagnostics(report, "src/settings/section.tsx")).toEqual([]);
@@ -472,6 +470,14 @@ export type Item = TurnItem;
 import { appServerUserInputResponse } from "../../../../../app-server/protocol/server-requests";
 
 export const response = appServerUserInputResponse;
+`.trimStart(),
+    );
+    await writeFile(
+      path.join(cwd, "src/features/chat/ui/protocol-leak.tsx"),
+      `
+import { threadTokenUsageFromAppServerUsage } from "../../../app-server/protocol/runtime-metrics";
+
+export const value = 
{String(threadTokenUsageFromAppServerUsage)}
; `.trimStart(), ); await writeFile( @@ -510,6 +516,7 @@ export const response = [appServerUserInputResponse, runtimeMetrics] satisfies u "src/features/chat/application/pending-requests/pending-request-actions.ts", "src/features/chat/application/threads/history-controller.ts", "src/features/chat/panel/surface/message-stream-presenter.ts", + "src/features/chat/ui/protocol-leak.tsx", "src/features/chat/app-server/inbound/app-server-logs.ts", "src/features/chat/app-server/mappers/message-stream/turn-items.ts", "src/features/chat/app-server/inbound/server-requests/responses.ts", @@ -526,6 +533,7 @@ export const response = [appServerUserInputResponse, runtimeMetrics] satisfies u expect(pluginMessages(report, "src/features/chat/panel/surface/message-stream-presenter.ts")).toEqual([ APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, ]); + expect(pluginMessages(report, "src/features/chat/ui/protocol-leak.tsx")).toEqual([APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE]); expect(pluginMessages(report, "src/features/chat/app-server/inbound/app-server-logs.ts")).toEqual([ APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, ]); @@ -656,10 +664,10 @@ export const format = formatDate; const report = biomeLint(["src/app-server/protocol/diagnostics.ts", "src/shared/thread-picker.ts", "src/shared/date.ts"], cwd); expect(pluginMessages(report, "src/app-server/protocol/diagnostics.ts")).toEqual([ - "Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server adapters.", + "Do not import feature modules from this layer. Move shared behavior to shared, domain, or app-server adapters.", ]); expect(pluginMessages(report, "src/shared/thread-picker.ts")).toEqual([ - "Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server adapters.", + "Do not import feature modules from this layer. Move shared behavior to shared, domain, or app-server adapters.", ]); expect(pluginDiagnostics(report, "src/shared/date.ts")).toEqual([]); }); @@ -710,14 +718,14 @@ export type Client = AppServerClient; ); expect(pluginMessages(report, "src/app-server/protocol/catalog.ts")).toEqual([ - "App-server protocol, domain, and shared modules must not import app-server connection internals. Keep connection usage at app-server adapters.", + "Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", ]); expect(pluginDiagnostics(report, "src/app-server/services/catalog.ts")).toEqual([]); expect(pluginMessages(report, "src/domain/connection-client.ts")).toEqual([ - "App-server protocol, domain, and shared modules must not import app-server connection internals. Keep connection usage at app-server adapters.", + "Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", ]); expect(pluginMessages(report, "src/shared/connection-client.ts")).toEqual([ - "App-server protocol, domain, and shared modules must not import app-server connection internals. Keep connection usage at app-server adapters.", + "Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", ]); }); @@ -758,7 +766,7 @@ export type AppServerThreadResumeClient = Pick; ); expect(pluginMessages(report, "src/features/chat/application/threads/history.ts")).toEqual([ - "Keep app-server projection RPCs behind app-server facades; chat application code should consume Panel-owned snapshots or view models.", + "Keep app-server projection RPCs behind app-server facades; consume Panel-owned snapshots or view models here.", ]); expect(pluginDiagnostics(report, "src/app-server/threads.ts")).toEqual([]); expect(pluginDiagnostics(report, "src/features/chat/host/connection-bundle.ts")).toEqual([]);