Constrain app-server root module placement

This commit is contained in:
murashit 2026-06-27 19:47:47 +09:00
parent 7b1b789f3b
commit 19dc423f21
43 changed files with 182 additions and 70 deletions

View file

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

View file

@ -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.

View file

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

View file

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

View file

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

View file

@ -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,

View file

@ -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<T, K extends keyof Diagnostics["probes"]> {
value: T;

View file

@ -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<readonly Thread[]>;

View file

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

View file

@ -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[];

View file

@ -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"];

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

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

View file

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

View file

@ -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,

View file

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

View file

@ -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<M extends ServerNotificationMethod> = Extract<ServerNotification, { method: M }>;

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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", () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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,

View file

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

View file

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

View file

@ -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<void> {
`.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<void> {
await client.threadTurnsList("thread", null, 20);
@ -1165,7 +1228,7 @@ export type AppServerThreadResumeClient = Pick<AppServerClient, "resumeThread">;
"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,