From 19dc423f2175ad39c85b3b92d9ac82bb3b5e23d0 Mon Sep 17 00:00:00 2001 From: murashit Date: Sat, 27 Jun 2026 19:47:47 +0900 Subject: [PATCH] Constrain app-server root module placement --- biome.jsonc | 12 +++ docs/development.md | 2 +- .../lint/no-app-server-root-module-files.grit | 5 ++ .../no-app-server-root-module-imports.grit | 16 ++++ .../no-app-server-subfolder-root-imports.grit | 16 ++++ src/app-server/query/cache.ts | 4 +- src/app-server/query/metadata-probes.ts | 2 +- src/app-server/{ => query}/thread-catalog.ts | 4 +- .../{route-scope.ts => routing/scope.ts} | 0 .../{ => routing}/server-requests.ts | 8 +- src/app-server/{ => services}/catalog.ts | 6 +- .../services/ephemeral-structured-turn.ts | 2 +- src/app-server/services/runtime-overrides.ts | 2 +- src/app-server/services/thread-archive.ts | 2 +- src/app-server/{ => services}/threads.ts | 28 +++---- .../{ => services}/tool-inventory.ts | 10 +-- .../chat/app-server/actions/diagnostics.ts | 2 +- .../chat/app-server/actions/threads.ts | 4 +- .../chat/app-server/goals/transport.ts | 2 +- .../chat/app-server/inbound/handler.ts | 4 +- .../app-server/inbound/notification-plan.ts | 4 +- .../inbound/notification-routing.ts | 2 +- .../references/thread-reference-resolver.ts | 2 +- .../chat/app-server/threads/projection.ts | 2 +- .../chat/app-server/threads/transport.ts | 2 +- src/features/chat/host/contracts.ts | 2 +- src/features/selection-rewrite/runner.ts | 2 +- src/features/thread-picker/modal.obsidian.ts | 2 +- src/features/threads-view/session.ts | 2 +- src/features/threads/thread-operations.ts | 2 +- src/features/threads/thread-title-service.ts | 2 +- src/plugin-runtime.ts | 2 +- src/settings/dynamic-sections-controller.ts | 4 +- src/settings/host.ts | 2 +- tests/app-server/catalog.test.ts | 2 +- tests/app-server/thread-activation.test.ts | 2 +- tests/app-server/thread-catalog.test.ts | 2 +- tests/app-server/threads.test.ts | 2 +- tests/app-server/tool-inventory.test.ts | 2 +- .../chat/app-server/inbound/handler.test.ts | 2 +- .../chat/app-server/inbound/routing.test.ts | 2 +- .../chat/host/view-connection.test.ts | 2 +- tests/scripts/grit-policy.test.mjs | 73 +++++++++++++++++-- 43 files changed, 182 insertions(+), 70 deletions(-) create mode 100644 scripts/lint/no-app-server-root-module-files.grit create mode 100644 scripts/lint/no-app-server-root-module-imports.grit create mode 100644 scripts/lint/no-app-server-subfolder-root-imports.grit rename src/app-server/{ => query}/thread-catalog.ts (99%) rename src/app-server/{route-scope.ts => routing/scope.ts} (100%) rename src/app-server/{ => routing}/server-requests.ts (97%) rename src/app-server/{ => services}/catalog.ts (94%) rename src/app-server/{ => services}/threads.ts (86%) rename src/app-server/{ => services}/tool-inventory.ts (96%) diff --git a/biome.jsonc b/biome.jsonc index 4f21ca59..ed718cc0 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -7,6 +7,18 @@ }, "plugins": [ // App-server protocol and generated binding boundaries. + { + "path": "./scripts/lint/no-app-server-root-module-files.grit", + "includes": ["**/src/app-server/*.ts"] + }, + { + "path": "./scripts/lint/no-app-server-root-module-imports.grit", + "includes": ["**/src/**/*.ts", "**/src/**/*.tsx"] + }, + { + "path": "./scripts/lint/no-app-server-subfolder-root-imports.grit", + "includes": ["**/src/app-server/**/*.ts"] + }, { "path": "./scripts/lint/no-app-server-projection-rpcs.grit", "includes": ["**/src/features/chat/application/**/*.ts"] diff --git a/docs/development.md b/docs/development.md index 8a57289e..bfd3b20a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -36,7 +36,7 @@ The generation script uses `codex app-server generate-ts --experimental` because The source tree is organized by implementation ownership, not by the single Obsidian plugin entrypoint: - `src/main.ts` and `src/plugin-runtime.ts` own Obsidian plugin registration, lifecycle wiring, and cross-surface runtime coordination. -- `src/app-server/` owns the app-server boundary, including connection code, protocol adapters, app-server services, shared queries, and generated payload adaptation. +- `src/app-server/` owns the app-server boundary through responsibility subfolders such as `connection/`, `protocol/`, `query/`, `routing/`, and `services/`. Do not add root-level app-server modules. - `src/features/` contains feature-owned surfaces and workflows. Feature-to-feature imports are acceptable only when the imported feature owns a concrete capability. - `src/workspace/` and `src/settings/` own Obsidian workspace coordination and settings-tab behavior. - `src/domain/` contains panel-owned, generated-independent meaning models and pure derivations. Domain code must not import app-server protocol, generated bindings, feature modules, UI, or Obsidian APIs. diff --git a/scripts/lint/no-app-server-root-module-files.grit b/scripts/lint/no-app-server-root-module-files.grit new file mode 100644 index 00000000..7146bb2d --- /dev/null +++ b/scripts/lint/no-app-server-root-module-files.grit @@ -0,0 +1,5 @@ +language js + +`$program` where { + register_diagnostic(span=$program, message="Keep app-server modules in responsibility subfolders; src/app-server root must not accumulate boundary adapters.", severity="error") +} diff --git a/scripts/lint/no-app-server-root-module-imports.grit b/scripts/lint/no-app-server-root-module-imports.grit new file mode 100644 index 00000000..943dc7b3 --- /dev/null +++ b/scripts/lint/no-app-server-root-module-imports.grit @@ -0,0 +1,16 @@ +language js + +private pattern js_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +js_module_reference() as $stmt where { + $stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\./)?app-server|(?:\.\./)+app-server|src/app-server)/[^/\"']+[\"']$" }, + register_diagnostic(span=$stmt, message="Import app-server boundary modules from responsibility subfolders, not src/app-server root modules.", severity="error") +} diff --git a/scripts/lint/no-app-server-subfolder-root-imports.grit b/scripts/lint/no-app-server-subfolder-root-imports.grit new file mode 100644 index 00000000..6381ad08 --- /dev/null +++ b/scripts/lint/no-app-server-subfolder-root-imports.grit @@ -0,0 +1,16 @@ +language js + +private pattern js_module_reference() { + or { + JsImport(), + JsExportNamedFromClause(), + JsExportFromClause(), + TsImportType(), + JsImportCallExpression() + } +} + +js_module_reference() as $stmt where { + $stmt <: contains `$source` where { $source <: r"^[\"']\.\./[^/\"']+[\"']$" }, + register_diagnostic(span=$stmt, message="App-server subfolders must not import sibling root modules; move the dependency into a responsibility subfolder.", severity="error") +} diff --git a/src/app-server/query/cache.ts b/src/app-server/query/cache.ts index e3ccc233..62fc9d39 100644 --- a/src/app-server/query/cache.ts +++ b/src/app-server/query/cache.ts @@ -4,11 +4,11 @@ import type { ObservedResult, ObservedResultListener } from "../../domain/observ import { createServerDiagnostics, diagnosticProbeError, diagnosticProbeOk, diagnosticsWithProbe } from "../../domain/server/diagnostics"; import type { SharedServerMetadata } from "../../domain/server/metadata"; import type { Thread } from "../../domain/threads/model"; -import { listModelMetadata } from "../catalog"; import type { AppServerClient } from "../connection/client"; import type { AppServerClientAccessOptions } from "../connection/client-access"; import { runtimeConfigSnapshotFromAppServerConfig } from "../protocol/runtime-config"; -import { listThreads } from "../threads"; +import { listModelMetadata } from "../services/catalog"; +import { listThreads } from "../services/threads"; import { type AppServerQueryContext, activeThreadsQueryKey, diff --git a/src/app-server/query/metadata-probes.ts b/src/app-server/query/metadata-probes.ts index 1801efec..f9ff3117 100644 --- a/src/app-server/query/metadata-probes.ts +++ b/src/app-server/query/metadata-probes.ts @@ -1,9 +1,9 @@ import type { SkillMetadata } from "../../domain/catalog/metadata"; import type { RateLimitSnapshot } from "../../domain/runtime/metrics"; import { type Diagnostics, diagnosticProbeError, diagnosticProbeOk } from "../../domain/server/diagnostics"; -import { listSkillCatalog } from "../catalog"; import type { AppServerClient } from "../connection/client"; import { accountRateLimitsSummaryFromResponse, rateLimitSnapshotFromAccountRateLimitsResponse } from "../protocol/runtime-metrics"; +import { listSkillCatalog } from "../services/catalog"; interface MetadataProbeResult { value: T; diff --git a/src/app-server/thread-catalog.ts b/src/app-server/query/thread-catalog.ts similarity index 99% rename from src/app-server/thread-catalog.ts rename to src/app-server/query/thread-catalog.ts index 04468a59..f4ac08b4 100644 --- a/src/app-server/thread-catalog.ts +++ b/src/app-server/query/thread-catalog.ts @@ -1,5 +1,5 @@ -import type { ObservedResultListener } from "../domain/observed-result"; -import type { Thread } from "../domain/threads/model"; +import type { ObservedResultListener } from "../../domain/observed-result"; +import type { Thread } from "../../domain/threads/model"; type ThreadListObserver = ObservedResultListener; diff --git a/src/app-server/route-scope.ts b/src/app-server/routing/scope.ts similarity index 100% rename from src/app-server/route-scope.ts rename to src/app-server/routing/scope.ts diff --git a/src/app-server/server-requests.ts b/src/app-server/routing/server-requests.ts similarity index 97% rename from src/app-server/server-requests.ts rename to src/app-server/routing/server-requests.ts index bb849d59..101496f1 100644 --- a/src/app-server/server-requests.ts +++ b/src/app-server/routing/server-requests.ts @@ -5,8 +5,8 @@ import type { PendingApproval, PendingMcpElicitation, PendingUserInput, -} from "../domain/pending-requests/model"; -import type { ServerRequest } from "./connection/rpc-messages"; +} from "../../domain/pending-requests/model"; +import type { ServerRequest } from "../connection/rpc-messages"; import { appServerApprovalRequest, appServerApprovalResponse, @@ -14,14 +14,14 @@ import { appServerMcpElicitationResponse, appServerUserInputRequest, appServerUserInputResponse, -} from "./protocol/server-requests"; +} from "../protocol/server-requests"; import { type ActiveRouteScope, fallbackMessageScope, isMessageScopeInActiveRouteScope, isTurnScopedMessageForIdleActiveThread, type MessageScope, -} from "./route-scope"; +} from "./scope"; export type ServerRequestRoute = | { kind: "approval"; request: ServerRequest; approval: PendingApproval } diff --git a/src/app-server/catalog.ts b/src/app-server/services/catalog.ts similarity index 94% rename from src/app-server/catalog.ts rename to src/app-server/services/catalog.ts index ed49e0ae..218a2b77 100644 --- a/src/app-server/catalog.ts +++ b/src/app-server/services/catalog.ts @@ -1,12 +1,12 @@ -import type { HookItem, ModelMetadata, SkillMetadata } from "../domain/catalog/metadata"; -import type { AppServerClient } from "./connection/client"; +import type { HookItem, ModelMetadata, SkillMetadata } from "../../domain/catalog/metadata"; +import type { AppServerClient } from "../connection/client"; import { appServerHookOperationFromHookItem, type CatalogModel, hookItemsFromCatalogHooks, modelMetadataFromCatalogModels, skillMetadataFromCatalogSkills, -} from "./protocol/catalog"; +} from "../protocol/catalog"; export interface HookCatalog { hooks: HookItem[]; diff --git a/src/app-server/services/ephemeral-structured-turn.ts b/src/app-server/services/ephemeral-structured-turn.ts index d12fd17d..dece04d9 100644 --- a/src/app-server/services/ephemeral-structured-turn.ts +++ b/src/app-server/services/ephemeral-structured-turn.ts @@ -1,5 +1,4 @@ import { listenAbortSignal } from "../../shared/lifecycle/abort-signal"; -import type { ModelMetadataClient } from "../catalog"; import { AppServerClient, type AppServerClientHandlers, @@ -9,6 +8,7 @@ import { import type { AppServerClientRequestPolicy } from "../connection/client-access"; import type { ServerNotification } from "../connection/rpc-messages"; import { lastAgentMessageTextFromTurnRecord, type TurnItem, type TurnRecord } from "../protocol/turn"; +import type { ModelMetadataClient } from "./catalog"; export type StructuredTurnOutputSchema = AppServerStartStructuredTurnOptions["outputSchema"]; diff --git a/src/app-server/services/runtime-overrides.ts b/src/app-server/services/runtime-overrides.ts index 86bc09b6..4d306e38 100644 --- a/src/app-server/services/runtime-overrides.ts +++ b/src/app-server/services/runtime-overrides.ts @@ -1,6 +1,6 @@ import type { ModelMetadata, ReasoningEffort } from "../../domain/catalog/metadata"; import { findModelMetadataByIdOrName, supportedEffortsForModelMetadata } from "../../domain/catalog/metadata"; -import { listModelMetadata, type ModelMetadataClient } from "../catalog"; +import { listModelMetadata, type ModelMetadataClient } from "./catalog"; export interface RuntimeOverrideSettings { model: string | null; diff --git a/src/app-server/services/thread-archive.ts b/src/app-server/services/thread-archive.ts index b9a9b306..007e2000 100644 --- a/src/app-server/services/thread-archive.ts +++ b/src/app-server/services/thread-archive.ts @@ -1,7 +1,7 @@ import type { ArchiveExportSettings } from "../../domain/threads/archive-markdown"; import type { AppServerClient } from "../connection/client"; -import { readThreadForArchiveExport } from "../threads"; import { type ArchiveExportDestination, exportArchivedThreadMarkdown } from "./thread-archive-markdown"; +import { readThreadForArchiveExport } from "./threads"; export interface ArchiveThreadOptions { settings: ArchiveExportSettings; diff --git a/src/app-server/threads.ts b/src/app-server/services/threads.ts similarity index 86% rename from src/app-server/threads.ts rename to src/app-server/services/threads.ts index 371a4418..de9c3886 100644 --- a/src/app-server/threads.ts +++ b/src/app-server/services/threads.ts @@ -1,21 +1,21 @@ -import { normalizeReasoningEffort } from "../domain/catalog/metadata"; -import type { ApprovalsReviewer, ServiceTier } from "../domain/runtime/policy"; -import { parseServiceTier } from "../domain/runtime/policy"; -import type { ThreadActivationSnapshot } from "../domain/threads/activation"; -import type { ArchiveThreadInput } from "../domain/threads/archive-markdown"; -import type { ThreadGoal, ThreadGoalUpdate } from "../domain/threads/goal"; -import type { HistoricalTurn } from "../domain/threads/history"; -import type { Thread } from "../domain/threads/model"; -import { REFERENCED_THREAD_TURN_LIMIT } from "../domain/threads/reference"; -import type { ThreadConversationSummary } from "../domain/threads/transcript"; -import type { AppServerClient } from "./connection/client"; -import { type ThreadRecord, threadFromThreadRecord, threadsFromThreadRecords } from "./protocol/thread"; -import { appServerThreadGoalUserHistoryItem, threadGoalFromAppServerGoal } from "./protocol/thread-goal"; +import { normalizeReasoningEffort } from "../../domain/catalog/metadata"; +import type { ApprovalsReviewer, ServiceTier } from "../../domain/runtime/policy"; +import { parseServiceTier } from "../../domain/runtime/policy"; +import type { ThreadActivationSnapshot } from "../../domain/threads/activation"; +import type { ArchiveThreadInput } from "../../domain/threads/archive-markdown"; +import type { ThreadGoal, ThreadGoalUpdate } from "../../domain/threads/goal"; +import type { HistoricalTurn } from "../../domain/threads/history"; +import type { Thread } from "../../domain/threads/model"; +import { REFERENCED_THREAD_TURN_LIMIT } from "../../domain/threads/reference"; +import type { ThreadConversationSummary } from "../../domain/threads/transcript"; +import type { AppServerClient } from "../connection/client"; +import { type ThreadRecord, threadFromThreadRecord, threadsFromThreadRecords } from "../protocol/thread"; +import { appServerThreadGoalUserHistoryItem, threadGoalFromAppServerGoal } from "../protocol/thread-goal"; import { chronologicalConversationSummariesFromTurnRecords, completedConversationSummariesFromTurnRecords, transcriptEntriesFromTurnRecords, -} from "./protocol/turn"; +} from "../protocol/turn"; const THREAD_LIST_PAGE_LIMIT = 100; diff --git a/src/app-server/tool-inventory.ts b/src/app-server/services/tool-inventory.ts similarity index 96% rename from src/app-server/tool-inventory.ts rename to src/app-server/services/tool-inventory.ts index f240cea6..72b80b34 100644 --- a/src/app-server/tool-inventory.ts +++ b/src/app-server/services/tool-inventory.ts @@ -1,4 +1,4 @@ -import type { SkillMetadata } from "../domain/catalog/metadata"; +import type { SkillMetadata } from "../../domain/catalog/metadata"; import { type DiagnosticProbeResult, diagnosticProbeError, @@ -6,16 +6,16 @@ import { type McpServerDiagnostic, type McpServerStatusSummary, mcpServerStatusSummariesFromStatuses, -} from "../domain/server/diagnostics"; +} from "../../domain/server/diagnostics"; import type { ToolInventoryApp, ToolInventoryMarketplaceError, ToolInventoryPlugin, ToolInventorySnapshot, -} from "../domain/server/tool-inventory"; +} from "../../domain/server/tool-inventory"; +import type { AppServerClient } from "../connection/client"; +import { toolInventoryAppsFromAppInfos, toolInventoryPluginsFromInstalledResponse } from "../protocol/tool-inventory"; import { listSkillCatalog } from "./catalog"; -import type { AppServerClient } from "./connection/client"; -import { toolInventoryAppsFromAppInfos, toolInventoryPluginsFromInstalledResponse } from "./protocol/tool-inventory"; const APP_PAGE_LIMIT = 100; const APP_PAGE_LOOP_LIMIT = 20; diff --git a/src/features/chat/app-server/actions/diagnostics.ts b/src/features/chat/app-server/actions/diagnostics.ts index ee64e8e1..094d28df 100644 --- a/src/features/chat/app-server/actions/diagnostics.ts +++ b/src/features/chat/app-server/actions/diagnostics.ts @@ -1,6 +1,6 @@ import type { AppServerClient } from "../../../../app-server/connection/client"; import { readRateLimitMetadataProbe } from "../../../../app-server/query/metadata-probes"; -import { readToolInventory } from "../../../../app-server/tool-inventory"; +import { readToolInventory } from "../../../../app-server/services/tool-inventory"; import { cloneServerDiagnostics, type DiagnosticProbeMethod, diff --git a/src/features/chat/app-server/actions/threads.ts b/src/features/chat/app-server/actions/threads.ts index f8834e42..daacc850 100644 --- a/src/features/chat/app-server/actions/threads.ts +++ b/src/features/chat/app-server/actions/threads.ts @@ -1,5 +1,5 @@ -import type { ThreadCatalogEvent } from "../../../../app-server/thread-catalog"; -import { threadActivationSnapshotFromAppServerResponse } from "../../../../app-server/threads"; +import type { ThreadCatalogEvent } from "../../../../app-server/query/thread-catalog"; +import { threadActivationSnapshotFromAppServerResponse } from "../../../../app-server/services/threads"; import { runtimeConfigOrDefault } from "../../../../domain/runtime/config"; import type { Thread } from "../../../../domain/threads/model"; import { resumedThreadAction } from "../../application/state/actions"; diff --git a/src/features/chat/app-server/goals/transport.ts b/src/features/chat/app-server/goals/transport.ts index 17941dc7..ec4182c6 100644 --- a/src/features/chat/app-server/goals/transport.ts +++ b/src/features/chat/app-server/goals/transport.ts @@ -1,4 +1,4 @@ -import { readThreadGoal, recordThreadGoalUserMessage, setThreadGoal } from "../../../../app-server/threads"; +import { readThreadGoal, recordThreadGoalUserMessage, setThreadGoal } from "../../../../app-server/services/threads"; import type { ThreadGoalReadTransport, ThreadGoalTransport } from "../../application/threads/goal-transport"; import type { ConnectedChatAppServerClientHost, CurrentChatAppServerClientHost } from "../client-scope"; import { chatAppServerClientIsStale, withConnectedChatAppServerClient, withCurrentChatAppServerClient } from "../client-scope"; diff --git a/src/features/chat/app-server/inbound/handler.ts b/src/features/chat/app-server/inbound/handler.ts index 864a5429..04824deb 100644 --- a/src/features/chat/app-server/inbound/handler.ts +++ b/src/features/chat/app-server/inbound/handler.ts @@ -1,12 +1,12 @@ import type { RequestId, ServerNotification, ServerRequest } from "../../../../app-server/connection/rpc-messages"; +import type { ThreadCatalogEvent } from "../../../../app-server/query/thread-catalog"; import { routeServerRequest, serverRequestApprovalResponse, serverRequestCurrentTimeResponse, serverRequestMcpElicitationResponse, serverRequestUserInputResponse, -} from "../../../../app-server/server-requests"; -import type { ThreadCatalogEvent } from "../../../../app-server/thread-catalog"; +} from "../../../../app-server/routing/server-requests"; import { type ApprovalAction, contentForPendingMcpElicitation, diff --git a/src/features/chat/app-server/inbound/notification-plan.ts b/src/features/chat/app-server/inbound/notification-plan.ts index f1d7f2a1..e2c18c5c 100644 --- a/src/features/chat/app-server/inbound/notification-plan.ts +++ b/src/features/chat/app-server/inbound/notification-plan.ts @@ -1,6 +1,6 @@ import type { ServerNotification } from "../../../../app-server/connection/rpc-messages"; -import type { ThreadCatalogEvent } from "../../../../app-server/thread-catalog"; -import { threadFromAppServerRecord } from "../../../../app-server/threads"; +import type { ThreadCatalogEvent } from "../../../../app-server/query/thread-catalog"; +import { threadFromAppServerRecord } from "../../../../app-server/services/threads"; import { threadTokenUsageFromRuntimeUsage } from "../../../../domain/runtime/metrics"; import { normalizeExplicitThreadName } from "../../../../domain/threads/model"; import type { ThreadConversationSummary } from "../../../../domain/threads/transcript"; diff --git a/src/features/chat/app-server/inbound/notification-routing.ts b/src/features/chat/app-server/inbound/notification-routing.ts index 68a25fae..fea7ab34 100644 --- a/src/features/chat/app-server/inbound/notification-routing.ts +++ b/src/features/chat/app-server/inbound/notification-routing.ts @@ -5,7 +5,7 @@ import { isMessageScopeInActiveRouteScope, isTurnScopedMessageForIdleActiveThread, type MessageScope, -} from "../../../../app-server/route-scope"; +} from "../../../../app-server/routing/scope"; type ServerNotificationMethod = ServerNotification["method"]; type RoutedNotification = Extract; diff --git a/src/features/chat/app-server/references/thread-reference-resolver.ts b/src/features/chat/app-server/references/thread-reference-resolver.ts index 72a7636c..ca7e1473 100644 --- a/src/features/chat/app-server/references/thread-reference-resolver.ts +++ b/src/features/chat/app-server/references/thread-reference-resolver.ts @@ -1,4 +1,4 @@ -import { readReferencedThreadConversationSummaries, type ThreadConversationSummaryClient } from "../../../../app-server/threads"; +import { readReferencedThreadConversationSummaries, type ThreadConversationSummaryClient } from "../../../../app-server/services/threads"; import { type CodexInput, codexTextInputWithAttachments } from "../../../../domain/chat/input"; import type { Thread } from "../../../../domain/threads/model"; import { REFERENCED_THREAD_TURN_LIMIT, referencedThreadPromptBundle } from "../../../../domain/threads/reference"; diff --git a/src/features/chat/app-server/threads/projection.ts b/src/features/chat/app-server/threads/projection.ts index 9721f4bb..85b322c9 100644 --- a/src/features/chat/app-server/threads/projection.ts +++ b/src/features/chat/app-server/threads/projection.ts @@ -1,5 +1,5 @@ import type { AppServerClient } from "../../../../app-server/connection/client"; -import { threadActivationSnapshotFromAppServerResponse } from "../../../../app-server/threads"; +import { threadActivationSnapshotFromAppServerResponse } from "../../../../app-server/services/threads"; import type { ThreadTurnsPage } from "../../../../domain/threads/history"; import type { ThreadHistoryPage, ThreadResumeSnapshot } from "../../application/threads/thread-loading-transport"; import { messageStreamItemsFromTurns } from "../mappers/message-stream/turn-items"; diff --git a/src/features/chat/app-server/threads/transport.ts b/src/features/chat/app-server/threads/transport.ts index 5dbe9795..dbb1b3c9 100644 --- a/src/features/chat/app-server/threads/transport.ts +++ b/src/features/chat/app-server/threads/transport.ts @@ -1,4 +1,4 @@ -import { compactThread, forkThread, rollbackThread } from "../../../../app-server/threads"; +import { compactThread, forkThread, rollbackThread } from "../../../../app-server/services/threads"; import type { ThreadMutationTransport, ThreadRollbackSnapshot } from "../../application/threads/thread-mutation-transport"; import type { ConnectedChatAppServerClientHost } from "../client-scope"; import { withConnectedChatAppServerClient } from "../client-scope"; diff --git a/src/features/chat/host/contracts.ts b/src/features/chat/host/contracts.ts index 312c13bf..8f775753 100644 --- a/src/features/chat/host/contracts.ts +++ b/src/features/chat/host/contracts.ts @@ -2,8 +2,8 @@ import type { App, Component, EventRef } from "obsidian"; import type { AppServerClient } from "../../../app-server/connection/client"; import type { AppServerQueryContext } from "../../../app-server/query/keys"; +import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../../app-server/query/thread-catalog"; import type { ArchiveExportDestination } from "../../../app-server/services/thread-archive-markdown"; -import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../../app-server/thread-catalog"; import type { ModelMetadata } from "../../../domain/catalog/metadata"; import type { ObservedResultListener } from "../../../domain/observed-result"; import type { SharedServerMetadata } from "../../../domain/server/metadata"; diff --git a/src/features/selection-rewrite/runner.ts b/src/features/selection-rewrite/runner.ts index 34ba8d17..65668a4b 100644 --- a/src/features/selection-rewrite/runner.ts +++ b/src/features/selection-rewrite/runner.ts @@ -1,5 +1,5 @@ -import type { ModelMetadataClient } from "../../app-server/catalog"; import type { AppServerClientHandlers } from "../../app-server/connection/client"; +import type { ModelMetadataClient } from "../../app-server/services/catalog"; import { type EphemeralStructuredTurnClient, runEphemeralStructuredTurnForLastAgentText, diff --git a/src/features/thread-picker/modal.obsidian.ts b/src/features/thread-picker/modal.obsidian.ts index 1fa5256d..715c4ab5 100644 --- a/src/features/thread-picker/modal.obsidian.ts +++ b/src/features/thread-picker/modal.obsidian.ts @@ -1,6 +1,6 @@ import { type App, Notice, Platform, SuggestModal } from "obsidian"; -import type { ThreadCatalogActiveReader } from "../../app-server/thread-catalog"; +import type { ThreadCatalogActiveReader } from "../../app-server/query/thread-catalog"; import { type Thread, threadRecencyAt } from "../../domain/threads/model"; import { threadDisplayTitle } from "../../domain/threads/title"; import { shortThreadId } from "../../shared/id/thread-id"; diff --git a/src/features/threads-view/session.ts b/src/features/threads-view/session.ts index cc7bc9af..f3c325b5 100644 --- a/src/features/threads-view/session.ts +++ b/src/features/threads-view/session.ts @@ -2,8 +2,8 @@ import { Notice } from "obsidian"; import type { AppServerClientAccess } from "../../app-server/connection/client-access"; import { isStaleAppServerSharedQueryContextError } from "../../app-server/query/shared-queries"; +import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../app-server/query/thread-catalog"; import type { ArchiveExportDestination } from "../../app-server/services/thread-archive-markdown"; -import type { ThreadCatalogActiveReader, ThreadCatalogEventSink } from "../../app-server/thread-catalog"; import type { ReasoningEffort } from "../../domain/catalog/metadata"; import type { ObservedResult } from "../../domain/observed-result"; import { observedInitialError, observedInitialLoading, observedValue } from "../../domain/observed-result"; diff --git a/src/features/threads/thread-operations.ts b/src/features/threads/thread-operations.ts index 873a51fd..458d238c 100644 --- a/src/features/threads/thread-operations.ts +++ b/src/features/threads/thread-operations.ts @@ -1,7 +1,7 @@ import type { AppServerClientAccess } from "../../app-server/connection/client-access"; +import type { ThreadCatalogEventSink } from "../../app-server/query/thread-catalog"; import { type ArchiveThreadResult, archiveThreadOnAppServer } from "../../app-server/services/thread-archive"; import type { ArchiveExportDestination } from "../../app-server/services/thread-archive-markdown"; -import type { ThreadCatalogEventSink } from "../../app-server/thread-catalog"; import type { ArchiveExportSettings } from "../../domain/threads/archive-markdown"; import { normalizeExplicitThreadName } from "../../domain/threads/model"; diff --git a/src/features/threads/thread-title-service.ts b/src/features/threads/thread-title-service.ts index 4e4ab829..46acb1d8 100644 --- a/src/features/threads/thread-title-service.ts +++ b/src/features/threads/thread-title-service.ts @@ -1,6 +1,6 @@ import type { AppServerClientAccess } from "../../app-server/connection/client-access"; import { generateThreadTitleWithCodex } from "../../app-server/services/thread-title-generation"; -import { readCompletedConversationSummariesPage } from "../../app-server/threads"; +import { readCompletedConversationSummariesPage } from "../../app-server/services/threads"; import type { ReasoningEffort } from "../../domain/catalog/metadata"; import { findThreadTitleContext, diff --git a/src/plugin-runtime.ts b/src/plugin-runtime.ts index bad3d8cc..9128daf3 100644 --- a/src/plugin-runtime.ts +++ b/src/plugin-runtime.ts @@ -5,7 +5,7 @@ import { withShortLivedAppServerClient } from "./app-server/connection/short-liv import { AppServerQueryCache } from "./app-server/query/cache"; import { type AppServerQueryContext, appServerQueryContextIsComplete } from "./app-server/query/keys"; import { AppServerSharedQueries } from "./app-server/query/shared-queries"; -import { createThreadCatalog, type ThreadCatalog, type ThreadCatalogEvent } from "./app-server/thread-catalog"; +import { createThreadCatalog, type ThreadCatalog, type ThreadCatalogEvent } from "./app-server/query/thread-catalog"; import { VIEW_TYPE_CODEX_THREADS, VIEW_TYPE_CODEX_TURN_DIFF } from "./constants"; import type { ChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; import { persistedChatTurnDiffViewState } from "./features/chat/domain/turn-diff"; diff --git a/src/settings/dynamic-sections-controller.ts b/src/settings/dynamic-sections-controller.ts index 343a0884..be32a6c1 100644 --- a/src/settings/dynamic-sections-controller.ts +++ b/src/settings/dynamic-sections-controller.ts @@ -1,7 +1,7 @@ -import { type HookCatalog, listHookCatalog, setHookItemEnabled, trustHookItem } from "../app-server/catalog"; import type { AppServerClient } from "../app-server/connection/client"; import { isStaleAppServerSharedQueryContextError } from "../app-server/query/shared-queries"; -import { restoreArchivedThread as restoreArchivedThreadOnAppServer } from "../app-server/threads"; +import { type HookCatalog, listHookCatalog, setHookItemEnabled, trustHookItem } from "../app-server/services/catalog"; +import { restoreArchivedThread as restoreArchivedThreadOnAppServer } from "../app-server/services/threads"; import type { HookItem, ModelMetadata, ReasoningEffort } from "../domain/catalog/metadata"; import { findModelMetadataByIdOrName, sortedModelMetadata, supportedEffortsForModelMetadata } from "../domain/catalog/metadata"; import type { ObservedResult } from "../domain/observed-result"; diff --git a/src/settings/host.ts b/src/settings/host.ts index 3ac9a422..c9ef5d74 100644 --- a/src/settings/host.ts +++ b/src/settings/host.ts @@ -1,5 +1,5 @@ import type { AppServerClientAccess } from "../app-server/connection/client-access"; -import type { ThreadCatalogArchivedReader, ThreadCatalogEventSink } from "../app-server/thread-catalog"; +import type { ThreadCatalogArchivedReader, ThreadCatalogEventSink } from "../app-server/query/thread-catalog"; import type { ModelMetadata } from "../domain/catalog/metadata"; import type { ObservedResultListener } from "../domain/observed-result"; import type { CodexPanelSettings } from "./model"; diff --git a/tests/app-server/catalog.test.ts b/tests/app-server/catalog.test.ts index d1583385..646e1dfa 100644 --- a/tests/app-server/catalog.test.ts +++ b/tests/app-server/catalog.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; -import { listHookCatalog, listSkillCatalog } from "../../src/app-server/catalog"; import type { AppServerClient } from "../../src/app-server/connection/client"; import { appServerHookOperationFromHookItem, @@ -7,6 +6,7 @@ import { modelMetadataFromCatalogModels, skillMetadataFromCatalogSkills, } from "../../src/app-server/protocol/catalog"; +import { listHookCatalog, listSkillCatalog } from "../../src/app-server/services/catalog"; import type { HookMetadata } from "../../src/generated/app-server/v2/HookMetadata"; import type { Model } from "../../src/generated/app-server/v2/Model"; import type { SkillMetadata } from "../../src/generated/app-server/v2/SkillMetadata"; diff --git a/tests/app-server/thread-activation.test.ts b/tests/app-server/thread-activation.test.ts index 2644ea71..c274d908 100644 --- a/tests/app-server/thread-activation.test.ts +++ b/tests/app-server/thread-activation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { threadActivationSnapshotFromAppServerResponse } from "../../src/app-server/threads"; +import { threadActivationSnapshotFromAppServerResponse } from "../../src/app-server/services/threads"; import type { Thread as AppServerThread } from "../../src/generated/app-server/v2/Thread"; import type { ThreadResumeResponse } from "../../src/generated/app-server/v2/ThreadResumeResponse"; diff --git a/tests/app-server/thread-catalog.test.ts b/tests/app-server/thread-catalog.test.ts index 2f610708..45460390 100644 --- a/tests/app-server/thread-catalog.test.ts +++ b/tests/app-server/thread-catalog.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, type Mock, vi } from "vitest"; import { AppServerQueryCache } from "../../src/app-server/query/cache"; import { AppServerSharedQueries } from "../../src/app-server/query/shared-queries"; -import { createThreadCatalog, type ThreadCatalogEventObserver } from "../../src/app-server/thread-catalog"; +import { createThreadCatalog, type ThreadCatalogEventObserver } from "../../src/app-server/query/thread-catalog"; import type { Thread } from "../../src/domain/threads/model"; describe("ThreadCatalog", () => { diff --git a/tests/app-server/threads.test.ts b/tests/app-server/threads.test.ts index 5e8992c9..a2f02e6e 100644 --- a/tests/app-server/threads.test.ts +++ b/tests/app-server/threads.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClient } from "../../src/app-server/connection/client"; -import { listThreads } from "../../src/app-server/threads"; +import { listThreads } from "../../src/app-server/services/threads"; describe("app-server thread response adapters", () => { it("maps listed threads to domain threads with archive state", async () => { diff --git a/tests/app-server/tool-inventory.test.ts b/tests/app-server/tool-inventory.test.ts index 35729426..33124b22 100644 --- a/tests/app-server/tool-inventory.test.ts +++ b/tests/app-server/tool-inventory.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClient } from "../../src/app-server/connection/client"; -import { readToolInventory } from "../../src/app-server/tool-inventory"; +import { readToolInventory } from "../../src/app-server/services/tool-inventory"; describe("tool inventory", () => { it("reads plugin details with exactly one marketplace locator", async () => { diff --git a/tests/features/chat/app-server/inbound/handler.test.ts b/tests/features/chat/app-server/inbound/handler.test.ts index fadcddf2..371631a5 100644 --- a/tests/features/chat/app-server/inbound/handler.test.ts +++ b/tests/features/chat/app-server/inbound/handler.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import type { ServerNotification, ServerRequest } from "../../../../../src/app-server/connection/rpc-messages"; import { appServerApprovalRequest, appServerUserInputRequest } from "../../../../../src/app-server/protocol/server-requests"; import type { TurnRecord } from "../../../../../src/app-server/protocol/turn"; -import type { ThreadCatalogEvent } from "../../../../../src/app-server/thread-catalog"; +import type { ThreadCatalogEvent } from "../../../../../src/app-server/query/thread-catalog"; import type { Thread as PanelThread } from "../../../../../src/domain/threads/model"; import { type ChatInboundHandler, diff --git a/tests/features/chat/app-server/inbound/routing.test.ts b/tests/features/chat/app-server/inbound/routing.test.ts index 32f44c46..d03166cc 100644 --- a/tests/features/chat/app-server/inbound/routing.test.ts +++ b/tests/features/chat/app-server/inbound/routing.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import type { ServerNotification, ServerRequest } from "../../../../../src/app-server/connection/rpc-messages"; -import { routeServerRequest } from "../../../../../src/app-server/server-requests"; +import { routeServerRequest } from "../../../../../src/app-server/routing/server-requests"; import { planChatNotification } from "../../../../../src/features/chat/app-server/inbound/notification-plan"; import { routeServerNotification } from "../../../../../src/features/chat/app-server/inbound/notification-routing"; import { chatStateFixture, chatStateWith } from "../../support/state"; diff --git a/tests/features/chat/host/view-connection.test.ts b/tests/features/chat/host/view-connection.test.ts index ca98d6cd..820dd82b 100644 --- a/tests/features/chat/host/view-connection.test.ts +++ b/tests/features/chat/host/view-connection.test.ts @@ -4,7 +4,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite import type { ServerNotification } from "../../../../src/app-server/connection/rpc-messages"; import { modelMetadataFromCatalogModels } from "../../../../src/app-server/protocol/catalog"; import type { ThreadRecord } from "../../../../src/app-server/protocol/thread"; -import type { ThreadCatalogEvent } from "../../../../src/app-server/thread-catalog"; +import type { ThreadCatalogEvent } from "../../../../src/app-server/query/thread-catalog"; import type { ModelMetadata } from "../../../../src/domain/catalog/metadata"; import type { ObservedResult } from "../../../../src/domain/observed-result"; import { emptyRuntimeConfigSnapshot } from "../../../../src/domain/runtime/config"; diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index 9ab8da7e..421d172d 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -16,6 +16,12 @@ const projectPluginByName = new Map( ); const APP_SERVER_PROTOCOL_BOUNDARY_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 at its app-server boundary; feature state and UI must use Panel-owned models."; +const APP_SERVER_ROOT_MODULE_FILE_MESSAGE = + "Keep app-server modules in responsibility subfolders; src/app-server root must not accumulate boundary adapters."; +const APP_SERVER_ROOT_MODULE_IMPORT_MESSAGE = + "Import app-server boundary modules from responsibility subfolders, not src/app-server root modules."; +const APP_SERVER_SUBFOLDER_ROOT_IMPORT_MESSAGE = + "App-server subfolders must not import sibling root modules; move the dependency into a responsibility subfolder."; const CHAT_APPLICATION_OUTER_LAYER_MESSAGE = "Chat application modules must not import app-server, host, panel, presentation, or UI layers; expose state and workflow contracts instead."; const CHAT_APP_SERVER_OUTER_LAYER_MESSAGE = "Chat app-server adapters must not import chat host, panel, presentation, or UI layers."; @@ -705,13 +711,70 @@ export const value = statusText; ]); }); + it("keeps app-server root from becoming a boundary escape hatch", async () => { + const cwd = await tempBiomeWorkspace([ + "no-app-server-root-module-files.grit", + "no-app-server-root-module-imports.grit", + "no-app-server-subfolder-root-imports.grit", + ]); + await writeFile( + path.join(cwd, "src/app-server/escape.ts"), + ` +import type { AppServerClient } from "./connection/client"; + +export type Escape = AppServerClient; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/features/chat/app-server/root-import.ts"), + ` +import { listThreads } from "../../../../app-server/threads"; + +export const read = listThreads; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/app-server/services/root-import.ts"), + ` +import { listThreads } from "../threads"; + +export const read = listThreads; +`.trimStart(), + ); + await writeFile( + path.join(cwd, "src/app-server/services/allowed.ts"), + ` +import type { AppServerClient } from "../connection/client"; +import { listThreads } from "./threads"; + +export type Allowed = AppServerClient; +export const read = listThreads; +`.trimStart(), + ); + + const report = biomeLint( + [ + "src/app-server/escape.ts", + "src/features/chat/app-server/root-import.ts", + "src/app-server/services/root-import.ts", + "src/app-server/services/allowed.ts", + ], + cwd, + ); + + expect(pluginMessages(report, "src/app-server/escape.ts")).toEqual([APP_SERVER_ROOT_MODULE_FILE_MESSAGE]); + expect(pluginMessages(report, "src/features/chat/app-server/root-import.ts")).toEqual([APP_SERVER_ROOT_MODULE_IMPORT_MESSAGE]); + expect(pluginMessages(report, "src/app-server/services/root-import.ts")).toEqual([APP_SERVER_SUBFOLDER_ROOT_IMPORT_MESSAGE]); + expect(pluginDiagnostics(report, "src/app-server/services/allowed.ts")).toEqual([]); + }); + it("keeps chat application app-server projection RPCs behind facades", async () => { const report = await appServerBoundaryPolicyReport(); expect(pluginMessages(report, "src/features/chat/application/threads/history.ts")).toEqual([ "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/app-server/services/threads.ts")).toEqual([]); expect(pluginDiagnostics(report, "src/features/chat/host/connection-bundle.ts")).toEqual([]); }); @@ -999,7 +1062,7 @@ export const response = [appServerUserInputResponse, runtimeMetrics] satisfies u await writeFile( path.join(cwd, "src/domain/threads/model.ts"), ` -import { listThreads } from "../../app-server/threads"; +import { listThreads } from "../../app-server/services/threads"; import type { ThreadPickerModal } from "../../features/thread-picker/modal"; import { copyText } from "../../shared/ui/clipboard"; import type { App } from "obsidian"; @@ -1117,9 +1180,9 @@ export async function read(appServerClient: AppServerClient): Promise { `.trimStart(), ); await writeFile( - path.join(cwd, "src/app-server/threads.ts"), + path.join(cwd, "src/app-server/services/threads.ts"), ` -import type { AppServerClient } from "./connection/client"; +import type { AppServerClient } from "../connection/client"; export async function read(client: AppServerClient): Promise { await client.threadTurnsList("thread", null, 20); @@ -1165,7 +1228,7 @@ export type AppServerThreadResumeClient = Pick; "src/domain/connection-client.ts", "src/shared/connection-client.ts", "src/features/chat/application/threads/history.ts", - "src/app-server/threads.ts", + "src/app-server/services/threads.ts", "src/features/chat/host/connection-bundle.ts", ], cwd,