diff --git a/biome.jsonc b/biome.jsonc index 08af240f..dbbd8f56 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -16,14 +16,12 @@ // Chat feature architecture. "./scripts/lint/no-chat-domain-outer-layer-imports.grit", "./scripts/lint/no-chat-signal-imports.grit", - "./scripts/lint/no-chat-state-escape-hatches.grit", "./scripts/lint/no-implicit-dom-bridges.grit", "./scripts/lint/no-pure-chat-state-side-effects.grit", "./scripts/lint/no-ui-root-imports.grit", // Project-wide source shape. "./scripts/lint/no-handwritten-reexports.grit", - "./scripts/lint/no-non-reexport-index.grit", "./scripts/lint/no-self-referential-initializer-callback.grit", "./scripts/lint/no-unsafe-iterator-value.grit", "./scripts/lint/no-uncontrolled-preact-form-state.grit", diff --git a/scripts/lint/no-app-server-projection-rpcs.grit b/scripts/lint/no-app-server-projection-rpcs.grit index a3dea267..b5f7d042 100644 --- a/scripts/lint/no-app-server-projection-rpcs.grit +++ b/scripts/lint/no-app-server-projection-rpcs.grit @@ -2,11 +2,12 @@ language js or { or { - `client.resumeThread($...)` as $stmt, - `client.threadTurnsList($...)` as $stmt, - `client.forkThread($...)` as $stmt, - `client.rollbackThread($...)` as $stmt + `$client.resumeThread($...)` as $stmt, + `$client.threadTurnsList($...)` as $stmt, + `$client.forkThread($...)` as $stmt, + `$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 fe3624aa..f5a8337a 100644 --- a/scripts/lint/no-app-server-protocol-boundary-imports.grit +++ b/scripts/lint/no-app-server-protocol-boundary-imports.grit @@ -1,13 +1,17 @@ language js -or { +private pattern js_module_reference() { or { - JsImport() as $stmt, - JsExportNamedFromClause() as $stmt, - JsExportFromClause() as $stmt, - TsImportType() as $stmt, - JsImportCallExpression() as $stmt - } where { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +or { + js_module_reference() as $stmt where { $stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$" }, @@ -21,18 +25,12 @@ or { }, { $filename <: r".*/src/features/chat/app-server/(?:inbound/notification-plan|mappers/message-stream/turn-items)\.ts$", - $stmt <: contains `$source` where { - not { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol/turn|src/app-server/protocol/turn)(?:/.*)?[\"']$" } - }, + not { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol/turn|src/app-server/protocol/turn)(?:/.*)?[\"']$" }, register_diagnostic(span=$stmt, message="Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.", severity="error") }, { $filename <: r".*/src/features/chat/app-server/inbound/(?:handler|routing)\.ts$", - $stmt <: contains `$source` where { - not { - $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol/server-requests|src/app-server/protocol/server-requests)(?:/.*)?[\"']$" - } - }, + not { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol/server-requests|src/app-server/protocol/server-requests)(?:/.*)?[\"']$" }, register_diagnostic(span=$stmt, message="Chat app-server request handling may consume server request protocol projections only. Convert app-server payloads to chat pending request domain models at this boundary.", severity="error") } } @@ -44,5 +42,17 @@ or { 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") + }, + `require($source)` as $stmt where { + $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$", + $filename <: r".*/src/features/chat/app-server/(?:inbound/notification-plan|mappers/message-stream/turn-items)\.ts$", + not { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol/turn|src/app-server/protocol/turn)(?:/.*)?[\"']$" }, + register_diagnostic(span=$stmt, message="Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.", severity="error") + }, + `require($source)` as $stmt where { + $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$", + $filename <: r".*/src/features/chat/app-server/inbound/(?:handler|routing)\.ts$", + not { $source <: r"^[\"'](?:(?:\.\./)+app-server/protocol/server-requests|src/app-server/protocol/server-requests)(?:/.*)?[\"']$" }, + register_diagnostic(span=$stmt, message="Chat app-server request handling may consume server request protocol projections only. Convert app-server payloads to chat pending request domain models at this boundary.", 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 91ffa0a1..bdc860b7 100644 --- a/scripts/lint/no-chat-domain-outer-layer-imports.grit +++ b/scripts/lint/no-chat-domain-outer-layer-imports.grit @@ -1,12 +1,26 @@ language js -JsImport() as $stmt where { - $filename <: r".*/src/features/chat/domain/.*", - $stmt <: contains `$source` where { - or { - $source <: r"^[\"'](?:\.\./)+(?:app-server|application|host|panel|presentation|ui)(?:/.*)?[\"']$", - $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") +private pattern js_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +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-chat-signal-imports.grit b/scripts/lint/no-chat-signal-imports.grit index bf92f692..d99816ca 100644 --- a/scripts/lint/no-chat-signal-imports.grit +++ b/scripts/lint/no-chat-signal-imports.grit @@ -1,7 +1,24 @@ language js -JsImport() 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") +private pattern js_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +or { + 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") + }, + `require($source)` as $stmt 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") + } } diff --git a/scripts/lint/no-chat-state-escape-hatches.grit b/scripts/lint/no-chat-state-escape-hatches.grit deleted file mode 100644 index fddcfb4a..00000000 --- a/scripts/lint/no-chat-state-escape-hatches.grit +++ /dev/null @@ -1,5 +0,0 @@ -language js - -`"state/patched"` as $stmt where { - register_diagnostic(span=$stmt, message="Use a named ChatAction instead of reintroducing the generic state patch escape hatch.", 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 b51ef83f..543bb7d7 100644 --- a/scripts/lint/no-generated-app-server-boundary-imports.grit +++ b/scripts/lint/no-generated-app-server-boundary-imports.grit @@ -1,26 +1,26 @@ language js -or { +private pattern js_module_reference() { or { - JsImport() as $stmt, - JsExportNamedFromClause() as $stmt, - JsExportFromClause() as $stmt, - TsImportType() as $stmt, - JsImportCallExpression() as $stmt - } where { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +or { + js_module_reference() as $stmt where { $stmt <: contains `$source` where { $source <: r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$" }, - not { $filename <: r".*/src/generated/.*" }, - not { $filename <: r".*/src/app-server/connection/.*" }, + not { $filename <: r".*/(?:src/generated|src/app-server/connection|tests/app-server)/.*" }, not { $filename <: r".*/src/app-server/protocol/(?:turn|server-requests)\.ts$" }, - not { $filename <: r".*/tests/app-server/.*" }, 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") }, `require($source)` as $stmt where { $source <: r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$", - not { $filename <: r".*/src/generated/.*" }, - not { $filename <: r".*/src/app-server/connection/.*" }, + not { $filename <: r".*/(?:src/generated|src/app-server/connection|tests/app-server)/.*" }, not { $filename <: r".*/src/app-server/protocol/(?:turn|server-requests)\.ts$" }, - not { $filename <: r".*/tests/app-server/.*" }, 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") } } diff --git a/scripts/lint/no-generated-app-server-import-shapes.grit b/scripts/lint/no-generated-app-server-import-shapes.grit index 9191df83..9af38a1b 100644 --- a/scripts/lint/no-generated-app-server-import-shapes.grit +++ b/scripts/lint/no-generated-app-server-import-shapes.grit @@ -1,16 +1,43 @@ language js +private pattern js_non_import_module_reference() { + or { + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +private pattern js_static_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause() + } +} + +private pattern js_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + or { JsImport() as $stmt where { $stmt <: contains `$source` where { $source <: r"^[\"'].*generated/app-server/v2/Thread[\"']$" }, - $stmt <: contains `Thread`, + not { $stmt <: r".*\bThread\s+as\s+AppServerThread\b.*" }, register_diagnostic(span=$stmt, message="Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.", severity="error") }, - or { - JsImport() as $stmt, - JsExportNamedFromClause() as $stmt, - JsExportFromClause() as $stmt - } where { + js_non_import_module_reference() as $stmt where { + $stmt <: contains `$source` where { $source <: r"^[\"'].*generated/app-server/v2/Thread[\"']$" }, + register_diagnostic(span=$stmt, message="Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.", severity="error") + }, + js_static_module_reference() as $stmt where { $filename <: r".*/src/app-server/protocol/turn\.ts$", $stmt <: contains `$source` where { $source <: r"^[\"'].*generated/app-server/.*[\"']$", @@ -18,16 +45,18 @@ or { }, register_diagnostic(span=$stmt, message="Only generated ThreadItem is allowed in the turn protocol exception. Model other turn payload shapes locally.", severity="error") }, - or { - JsImport() as $stmt, - JsExportNamedFromClause() as $stmt, - JsExportFromClause() as $stmt - } where { + js_module_reference() as $stmt where { $filename <: r".*/src/app-server/protocol/server-requests\.ts$", $stmt <: contains `$source` where { $source <: r"^[\"'].*generated/app-server/.*[\"']$", not { $source <: r"^[\"'].*generated/app-server/(?:RequestId|ServerRequest)[\"']$" } }, register_diagnostic(span=$stmt, message="Only generated RequestId and ServerRequest are allowed in the server request protocol exception.", severity="error") + }, + `require($source)` as $stmt where { + $filename <: r".*/src/app-server/protocol/server-requests\.ts$", + $source <: r"^[\"'].*generated/app-server/.*[\"']$", + not { $source <: r"^[\"'].*generated/app-server/(?:RequestId|ServerRequest)[\"']$" }, + register_diagnostic(span=$stmt, message="Only generated RequestId and ServerRequest are allowed in the server request protocol exception.", severity="error") } } diff --git a/scripts/lint/no-implicit-dom-bridges.grit b/scripts/lint/no-implicit-dom-bridges.grit index 9f50cf8d..ae302334 100644 --- a/scripts/lint/no-implicit-dom-bridges.grit +++ b/scripts/lint/no-implicit-dom-bridges.grit @@ -14,9 +14,7 @@ or { `$target.removeEventListener($...)` as $stmt } where { $filename <: r".*/src/.*\.tsx?$", - not { $filename <: r".*\.dom\.tsx?$" }, - not { $filename <: r".*\.obsidian\.tsx?$" }, - not { $filename <: r".*\.measure\.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 742441c0..5dd786d4 100644 --- a/scripts/lint/no-lower-level-boundary-imports.grit +++ b/scripts/lint/no-lower-level-boundary-imports.grit @@ -1,38 +1,54 @@ language js -or { +private pattern js_module_reference() { or { - JsImport() as $stmt, - JsExportNamedFromClause() as $stmt, - JsExportFromClause() as $stmt, - TsImportType() as $stmt, - JsImportCallExpression() as $stmt - } where { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +or { + js_module_reference() as $stmt where { + $filename <: r".*/src/domain/.*", + $stmt <: contains `$source` where { + or { + $source <: r"^[\"'](?:(?:\.\./)+(?:app-server|settings|workspace|shared/ui)|src/(?:app-server|settings|workspace|shared/ui))(?:/.*)?[\"']$", + $source <: r"^[\"']obsidian[\"']$" + } + }, + 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 { or { $source <: r"^[\"'](?:(?:\.\./)+features|src/features)(?:/.*)?[\"']$" } }, + $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") }, - or { - JsImport() as $stmt, - JsExportNamedFromClause() as $stmt, - JsExportFromClause() as $stmt, - TsImportType() as $stmt, - JsImportCallExpression() as $stmt - } where { - or { $filename <: r".*/src/app-server/protocol/.*", $filename <: r".*/src/(?:domain|shared)/.*" }, + js_module_reference() as $stmt where { + $filename <: r".*/src/(?:app-server/protocol|domain|shared)/.*", $stmt <: contains `$source` where { - or { $source <: r"^[\"'](?:(?:\.\./)+(?:app-server/)?connection|src/app-server/connection)(?:/.*)?[\"']$" } + $source <: r"^[\"'](?:(?:\.\./)+(?:app-server/)?connection|src/app-server/connection)(?:/.*)?[\"']$" }, 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") }, + `require($source)` as $stmt where { + $filename <: r".*/src/domain/.*", + or { + $source <: r"^[\"'](?:(?:\.\./)+(?:app-server|settings|workspace|shared/ui)|src/(?:app-server|settings|workspace|shared/ui))(?:/.*)?[\"']$", + $source <: r"^[\"']obsidian[\"']$" + }, + 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)/.*", - or { $source <: r"^[\"'](?:(?:\.\./)+features|src/features)(?:/.*)?[\"']$" }, + $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") }, `require($source)` as $stmt where { - or { $filename <: r".*/src/app-server/protocol/.*", $filename <: r".*/src/(?:domain|shared)/.*" }, - or { $source <: r"^[\"'](?:(?:\.\./)+(?:app-server/)?connection|src/app-server/connection)(?:/.*)?[\"']$" }, + $filename <: r".*/src/(?:app-server/protocol|domain|shared)/.*", + $source <: r"^[\"'](?:(?:\.\./)+(?:app-server/)?connection|src/app-server/connection)(?:/.*)?[\"']$", 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-non-reexport-index.grit b/scripts/lint/no-non-reexport-index.grit deleted file mode 100644 index 6c5334ea..00000000 --- a/scripts/lint/no-non-reexport-index.grit +++ /dev/null @@ -1,16 +0,0 @@ -language js - -file($name, $body) where { - $name <: r".*/src/.*/index\.tsx?$", - $body <: contains bubble or { - JsImport() as $stmt, - JsExport() as $stmt where { - not { - $stmt <: contains or { - JsExportNamedFromClause(), - JsExportFromClause() - } - } - } - } where { register_diagnostic(span=$stmt, message="Keep src index files as re-export-only boundaries.", severity="error") } -} diff --git a/scripts/lint/no-self-referential-initializer-callback.grit b/scripts/lint/no-self-referential-initializer-callback.grit index 8d439852..e457e215 100644 --- a/scripts/lint/no-self-referential-initializer-callback.grit +++ b/scripts/lint/no-self-referential-initializer-callback.grit @@ -1,14 +1,7 @@ language js -or { - `const $name = new $constructor($callback)` as $stmt where { - $callback <: `() => $body`, - $body <: contains `$name`, - register_diagnostic(span=$stmt, message="Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.", severity="error") - }, - `const $name = new $constructor($callback)` as $stmt where { - $callback <: `function() { $body }`, - $body <: contains `$name`, - register_diagnostic(span=$stmt, message="Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.", severity="error") - } +`const $name = new $constructor($callback)` as $stmt where { + or { $callback <: `() => $body`, $callback <: `function() { $body }` }, + $body <: contains `$name`, + register_diagnostic(span=$stmt, message="Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.", severity="error") } diff --git a/scripts/lint/no-ui-root-imports.grit b/scripts/lint/no-ui-root-imports.grit index 6026284b..7f7b0236 100644 --- a/scripts/lint/no-ui-root-imports.grit +++ b/scripts/lint/no-ui-root-imports.grit @@ -1,10 +1,26 @@ language js -JsImport() as $stmt where { - $filename <: r".*/src/.*", - $stmt <: contains `$source` where { $source <: r"^[\"'].*shared/ui/ui-root\.dom[\"']$" }, - not { $filename <: r".*\.dom\.tsx?$" }, - not { $filename <: r".*\.obsidian\.tsx?$" }, - not { $filename <: r".*\.measure\.tsx?$" }, - register_diagnostic(span=$stmt, message="Import the Preact root adapter only from explicit root bridge files.", severity="error") +private pattern js_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +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/app-server/ephemeral-structured-turn.test.ts b/tests/app-server/ephemeral-structured-turn.test.ts index 05f54233..2510d812 100644 --- a/tests/app-server/ephemeral-structured-turn.test.ts +++ b/tests/app-server/ephemeral-structured-turn.test.ts @@ -17,7 +17,7 @@ import type { RequestId } from "../../src/generated/app-server/RequestId"; import type { ServerNotification } from "../../src/generated/app-server/ServerNotification"; import type { ServerRequest } from "../../src/generated/app-server/ServerRequest"; import type { ModelListResponse } from "../../src/generated/app-server/v2/ModelListResponse"; -import type { Thread as ThreadRecord } from "../../src/generated/app-server/v2/Thread"; +import type { Thread as AppServerThread } from "../../src/generated/app-server/v2/Thread"; import type { ThreadStartResponse } from "../../src/generated/app-server/v2/ThreadStartResponse"; import type { TurnStartResponse } from "../../src/generated/app-server/v2/TurnStartResponse"; @@ -331,7 +331,7 @@ function threadStartResponse(threadId: string): ThreadStartResponse { }; } -function thread(id: string): ThreadRecord { +function thread(id: string): AppServerThread { return { id, sessionId: "session", diff --git a/tests/app-server/thread-activation.test.ts b/tests/app-server/thread-activation.test.ts index 8cf7769b..2644ea71 100644 --- a/tests/app-server/thread-activation.test.ts +++ b/tests/app-server/thread-activation.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { threadActivationSnapshotFromAppServerResponse } from "../../src/app-server/threads"; -import type { Thread as ThreadRecord } from "../../src/generated/app-server/v2/Thread"; +import type { Thread as AppServerThread } from "../../src/generated/app-server/v2/Thread"; import type { ThreadResumeResponse } from "../../src/generated/app-server/v2/ThreadResumeResponse"; describe("app-server thread activation", () => { @@ -21,7 +21,7 @@ describe("app-server thread activation", () => { }); }); -function responseFixture(thread: ThreadRecord): ThreadResumeResponse { +function responseFixture(thread: AppServerThread): ThreadResumeResponse { return { thread, model: "gpt-5.5", @@ -40,7 +40,7 @@ function responseFixture(thread: ThreadRecord): ThreadResumeResponse { }; } -function threadFixture(id: string, name: string): ThreadRecord { +function threadFixture(id: string, name: string): AppServerThread { return { id, sessionId: "session", diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index ae42c280..68df34ca 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -48,28 +48,35 @@ import { signal } from '@preact/signals'; export const status = signal("idle"); `.trimStart(), ); + await writeFile( + path.join(cwd, "src/shared/ui/signal-escapes.tsx"), + ` +export { signal } from "@preact/signals"; +export type SignalValue = import("@preact/signals").Signal; - const report = biomeLint(["src/features/chat/panel/shell-state.tsx", "src/shared/ui/components.tsx"], cwd); +export async function loadSignals() { + return import("@preact/signals"); +} + +const signals = require("@preact/signals"); +export const loadedSignals = signals; +`.trimStart(), + ); + + const report = biomeLint( + ["src/features/chat/panel/shell-state.tsx", "src/shared/ui/components.tsx", "src/shared/ui/signal-escapes.tsx"], + cwd, + ); 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.", ]); - }); - - it("keeps removed chat state escape hatches out of source", async () => { - const cwd = await tempBiomeWorkspace(["no-chat-state-escape-hatches.grit"]); - await writeFile( - path.join(cwd, "src/features/chat/application/state/root-reducer.ts"), - ` -export const actionType = "state/patched"; -`.trimStart(), - ); - - const report = biomeLint(["src/features/chat/application/state/root-reducer.ts"], cwd); - - expect(pluginMessages(report, "src/features/chat/application/state/root-reducer.ts")).toEqual([ - "Use a named ChatAction instead of reintroducing the generic state patch escape hatch.", + 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.", ]); }); @@ -91,11 +98,29 @@ export type View = Presenter; import type { MessageStreamItem } from "./item"; export type Item = MessageStreamItem; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/chat/domain/message-stream/outer-shapes.ts"), + ` +export { createChatStateStore } from "../../application/state/store"; +export type OuterStore = import("../../application/state/store").ChatStateStore; + +export async function loadComposer() { + return import("src/features/chat/ui/composer"); +} + +const host = require("../../host/session"); +export const outerHost = host; `.trimStart(), ); const report = biomeLint( - ["src/features/chat/domain/message-stream/selectors.ts", "src/features/chat/domain/message-stream/items.ts"], + [ + "src/features/chat/domain/message-stream/selectors.ts", + "src/features/chat/domain/message-stream/items.ts", + "src/features/chat/domain/message-stream/outer-shapes.ts", + ], cwd, ); @@ -104,6 +129,12 @@ export type Item = MessageStreamItem; "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.", ]); expect(pluginDiagnostics(report, "src/features/chat/domain/message-stream/items.ts")).toEqual([]); + expect(pluginMessages(report, "src/features/chat/domain/message-stream/outer-shapes.ts")).toEqual([ + "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.", + "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.", + "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.", + "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.", + ]); }); it("blocks generated app-server imports outside app-server boundaries", async () => { @@ -178,7 +209,9 @@ export type Item = TurnItem; ` import { toolInventoryAppsFromAppInfos } from "../../../../app-server/protocol/tool-inventory"; -export const convert = toolInventoryAppsFromAppInfos; +const toolInventory = require("../../../../app-server/protocol/tool-inventory"); + +export const convert = [toolInventoryAppsFromAppInfos, toolInventory]; `.trimStart(), ); await writeFile( @@ -208,6 +241,7 @@ export type Item = TurnItem; ]); expect(pluginMessages(report, "src/features/chat/app-server/inbound/notification-plan.ts")).toEqual([ "Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.", + "Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.", ]); expect(pluginDiagnostics(report, "src/features/chat/app-server/mappers/message-stream/turn-items.ts")).toEqual([]); }); @@ -228,6 +262,14 @@ export const response = appServerUserInputResponse; import { appServerUserInputResponse } from "../../../../app-server/protocol/server-requests"; export const response = appServerUserInputResponse; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/chat/app-server/inbound/handler.ts"), + ` +const runtimeMetrics = require("../../../../app-server/protocol/runtime-metrics"); + +export const response = runtimeMetrics; `.trimStart(), ); await writeFile( @@ -243,6 +285,7 @@ export const response = appServerUserInputResponse; [ "src/features/chat/panel/surface/message-stream-presenter.ts", "src/features/chat/app-server/inbound/app-server-logs.ts", + "src/features/chat/app-server/inbound/handler.ts", "src/features/chat/app-server/inbound/routing.ts", ], cwd, @@ -254,6 +297,9 @@ export const response = appServerUserInputResponse; expect(pluginMessages(report, "src/features/chat/app-server/inbound/app-server-logs.ts")).toEqual([ "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.", ]); + expect(pluginMessages(report, "src/features/chat/app-server/inbound/handler.ts")).toEqual([ + "Chat app-server request handling may consume server request protocol projections only. Convert app-server payloads to chat pending request domain models at this boundary.", + ]); expect(pluginDiagnostics(report, "src/features/chat/app-server/inbound/routing.ts")).toEqual([]); }); @@ -326,8 +372,37 @@ import { formatDate } from "./format"; export const format = formatDate; `.trimStart(), ); + await writeFile( + path.join(cwd, "src/domain/threads/model.ts"), + ` +import { listThreads } from "../../app-server/threads"; +import { copyText } from "../../shared/ui/clipboard"; +import type { App } from "obsidian"; - const report = biomeLint(["src/app-server/protocol/catalog.ts", "src/app-server/protocol/diagnostics.ts", "src/shared/date.ts"], cwd); +export type Host = App; +export const list = listThreads; +export const copy = copyText; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/domain/threads/format.ts"), + ` +import { formatDate } from "../../shared/date"; + +export const format = formatDate; +`.trimStart(), + ); + + const report = biomeLint( + [ + "src/app-server/protocol/catalog.ts", + "src/app-server/protocol/diagnostics.ts", + "src/shared/date.ts", + "src/domain/threads/model.ts", + "src/domain/threads/format.ts", + ], + cwd, + ); expect(pluginMessages(report, "src/app-server/protocol/catalog.ts")).toEqual([ "Lower-level modules must not import feature modules or app-server connection internals. Move shared behavior to shared, domain, or app-server adapters.", @@ -336,6 +411,12 @@ export const format = formatDate; "Lower-level modules must not import feature modules or app-server connection internals. Move shared behavior to shared, domain, or app-server adapters.", ]); expect(pluginDiagnostics(report, "src/shared/date.ts")).toEqual([]); + expect(pluginMessages(report, "src/domain/threads/model.ts")).toEqual([ + "Domain modules must stay pure and generated-independent; keep app-server, settings, workspace, UI, and Obsidian dependencies at boundary callers.", + "Domain modules must stay pure and generated-independent; keep app-server, settings, workspace, UI, and Obsidian dependencies at boundary callers.", + "Domain modules must stay pure and generated-independent; keep app-server, settings, workspace, UI, and Obsidian dependencies at boundary callers.", + ]); + expect(pluginDiagnostics(report, "src/domain/threads/format.ts")).toEqual([]); }); it("keeps generated app-server import shapes behind narrow aliases and protocol exceptions", async () => { @@ -354,6 +435,14 @@ export type ConnectionThread = Thread; import type { Thread as AppServerThread } from "../../generated/app-server/v2/Thread"; export type ConnectionThread = AppServerThread; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/app-server/connection/record-thread.ts"), + ` +import type { Thread as ThreadRecord } from "../../generated/app-server/v2/Thread"; + +export type ConnectionThread = ThreadRecord; `.trimStart(), ); await writeFile( @@ -374,6 +463,14 @@ import type { ToolRequestUserInputParams } from "../../generated/app-server/v2/T export type Request = ServerRequest; export type Params = ToolRequestUserInputParams; +export type InputParams = import("../../generated/app-server/v2/ToolRequestUserInputParams").ToolRequestUserInputParams; + +export async function loadParams() { + return import("../../generated/app-server/v2/ToolRequestUserInputParams"); +} + +const params = require("../../generated/app-server/v2/ToolRequestUserInputParams"); +export const loadedParams = params; `.trimStart(), ); @@ -381,6 +478,7 @@ export type Params = ToolRequestUserInputParams; [ "src/app-server/connection/thread.ts", "src/app-server/connection/aliased-thread.ts", + "src/app-server/connection/record-thread.ts", "src/app-server/protocol/turn.ts", "src/app-server/protocol/server-requests.ts", ], @@ -391,41 +489,20 @@ export type Params = ToolRequestUserInputParams; "Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.", ]); expect(pluginDiagnostics(report, "src/app-server/connection/aliased-thread.ts")).toEqual([]); + expect(pluginMessages(report, "src/app-server/connection/record-thread.ts")).toEqual([ + "Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.", + ]); expect(pluginMessages(report, "src/app-server/protocol/turn.ts")).toEqual([ "Only generated ThreadItem is allowed in the turn protocol exception. Model other turn payload shapes locally.", ]); expect(pluginMessages(report, "src/app-server/protocol/server-requests.ts")).toEqual([ "Only generated RequestId and ServerRequest are allowed in the server request protocol exception.", + "Only generated RequestId and ServerRequest are allowed in the server request protocol exception.", + "Only generated RequestId and ServerRequest are allowed in the server request protocol exception.", + "Only generated RequestId and ServerRequest are allowed in the server request protocol exception.", ]); }); - it("keeps src index files as re-export-only boundaries", async () => { - const cwd = await tempBiomeWorkspace(["no-non-reexport-index.grit"]); - await writeFile( - path.join(cwd, "src/features/chat/index.ts"), - ` -import { value } from "./value"; - -export const local = value; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/thread-picker/index.ts"), - ` -export { openThreadPicker } from "./modal"; -export type { ThreadPickerOptions } from "./types"; -`.trimStart(), - ); - - const report = biomeLint(["src/features/chat/index.ts", "src/features/thread-picker/index.ts"], cwd); - - expect(pluginMessages(report, "src/features/chat/index.ts")).toEqual([ - "Keep src index files as re-export-only boundaries.", - "Keep src index files as re-export-only boundaries.", - ]); - expect(pluginDiagnostics(report, "src/features/thread-picker/index.ts")).toEqual([]); - }); - it("keeps chat application app-server projection RPCs behind facades", async () => { const cwd = await tempBiomeWorkspace(["no-app-server-projection-rpcs.grit"]); await writeFile( @@ -433,8 +510,8 @@ export type { ThreadPickerOptions } from "./types"; ` import type { AppServerClient } from "../../../../app-server/connection/client"; -export async function read(client: AppServerClient): Promise { - await client.threadTurnsList("thread", null, 20); +export async function read(appServerClient: AppServerClient): Promise { + await appServerClient.threadTurnsList("thread", null, 20); } `.trimStart(), ); @@ -531,13 +608,36 @@ import { renderUiRoot } from "../../../shared/ui/ui-root.dom"; export const render = renderUiRoot; `.trimStart(), ); + await writeFile( + path.join(cwd, "src/features/chat/ui/root-escapes.tsx"), + ` +export { renderUiRoot } from "../../../shared/ui/ui-root.dom"; +export type RootRenderer = import("../../../shared/ui/ui-root.dom").RootRenderer; - const report = biomeLint(["src/features/chat/ui/composer.tsx", "src/features/chat/panel/shell.dom.tsx"], cwd); +export async function loadRoot() { + return import("../../../shared/ui/ui-root.dom"); +} + +const root = require("../../../shared/ui/ui-root.dom"); +export const loadedRoot = root; +`.trimStart(), + ); + + const report = biomeLint( + ["src/features/chat/ui/composer.tsx", "src/features/chat/panel/shell.dom.tsx", "src/features/chat/ui/root-escapes.tsx"], + cwd, + ); expect(pluginMessages(report, "src/features/chat/ui/composer.tsx")).toEqual([ "Import the Preact root adapter only from explicit root bridge files.", ]); expect(pluginDiagnostics(report, "src/features/chat/panel/shell.dom.tsx")).toEqual([]); + expect(pluginMessages(report, "src/features/chat/ui/root-escapes.tsx")).toEqual([ + "Import the Preact root adapter only from explicit root bridge files.", + "Import the Preact root adapter only from explicit root bridge files.", + "Import the Preact root adapter only from explicit root bridge files.", + "Import the Preact root adapter only from explicit root bridge files.", + ]); }); it("keeps chat state transforms pure and catches global scheduling calls", async () => { @@ -753,7 +853,7 @@ async function tempBiomeWorkspace(plugins) { await mkdir(path.join(cwd, "src/features/chat/panel"), { recursive: true }); await mkdir(path.join(cwd, "src/features/chat/panel/surface"), { recursive: true }); await mkdir(path.join(cwd, "src/features/chat/ui"), { recursive: true }); - await mkdir(path.join(cwd, "src/features/thread-picker"), { recursive: true }); + await mkdir(path.join(cwd, "src/domain/threads"), { recursive: true }); await mkdir(path.join(cwd, "src/app-server/connection"), { recursive: true }); await mkdir(path.join(cwd, "src/app-server/protocol"), { recursive: true }); await mkdir(path.join(cwd, "src/app-server/services"), { recursive: true });