Split shared thread capability folders

This commit is contained in:
murashit 2026-06-27 21:25:13 +09:00
parent a14e3d19e7
commit 727f5393af
18 changed files with 102 additions and 53 deletions

View file

@ -6,11 +6,23 @@
"useIgnoreFile": true
},
"plugins": [
// App-server protocol and generated binding boundaries.
// Shared source-shape boundaries.
{
"path": "./scripts/lint/no-app-server-root-module-files.grit",
"includes": ["**/src/app-server/*.ts"]
"path": "./scripts/lint/no-responsibility-root-module-files.grit",
"includes": [
"**/src/app-server/*.ts",
"**/src/app-server/*.tsx",
"**/src/domain/*.ts",
"**/src/domain/*.tsx",
"**/src/features/chat/*.ts",
"**/src/features/chat/*.tsx",
"**/src/features/threads/*.ts",
"**/src/features/threads/*.tsx",
"**/src/shared/*.ts",
"**/src/shared/*.tsx"
]
},
// App-server protocol and generated binding boundaries.
{
"path": "./scripts/lint/no-app-server-root-module-imports.grit",
"includes": ["**/src/**/*.ts", "**/src/**/*.tsx"]

View file

@ -1,5 +0,0 @@
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,5 @@
language js
`$program` where {
register_diagnostic(span=$program, message="Keep responsibility-split source roots free of module files; add modules to the matching subfolder instead of the root.", severity="error")
}

View file

@ -7,7 +7,7 @@ import {
type ThreadRenameLifecycleState,
threadRenameGenerationStillActive,
transitionThreadRenameLifecycleState,
} from "../../../threads/rename-lifecycle";
} from "../../../threads/list/rename-lifecycle";
import type { DisclosureSetAction } from "./actions";
import { patchObject } from "./patch";

View file

@ -4,8 +4,8 @@ import type { AppServerClientAccess } from "../../../app-server/connection/clien
import { recoverRolloutTokenUsage } from "../../../app-server/services/rollout-token-usage";
import { normalizeExplicitThreadName } from "../../../domain/threads/model";
import type { LocalIdSource } from "../../../shared/id/local-id";
import { createThreadOperations, type ThreadOperations } from "../../threads/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../../threads/thread-title-service";
import { createThreadOperations, type ThreadOperations } from "../../threads/workflows/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../../threads/workflows/thread-title-service";
import type { ChatServerThreadActions } from "../app-server/actions/threads";
import { createChatThreadGoalReadTransport, createChatThreadGoalTransport } from "../app-server/goals/transport";
import { createChatThreadHistoryTransport, createChatThreadResumeTransport } from "../app-server/threads/loading-transport";

View file

@ -3,7 +3,7 @@ import { h } from "preact";
import { CLIENT_VERSION } from "../../../../constants";
import type { Thread } from "../../../../domain/threads/model";
import { threadRowCoreProjection } from "../../../threads/row-projection";
import { threadRowCoreProjection } from "../../../threads/list/row-projection";
import { connectionDiagnosticSectionsFromState } from "../../application/connection/diagnostic-sections";
import { toolInventoryDiagnosticSections } from "../../application/connection/tool-inventory-diagnostic-sections";
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";

View file

@ -9,8 +9,8 @@ import type { ArchiveExportSettings } from "../../domain/threads/archive-markdow
import type { Thread } from "../../domain/threads/model";
import type { ObservedResult } from "../../shared/query/observed-result";
import { observedInitialError, observedInitialLoading, observedValue } from "../../shared/query/observed-result";
import { createThreadOperations, type ThreadOperations } from "../threads/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../threads/thread-title-service";
import { createThreadOperations, type ThreadOperations } from "../threads/workflows/thread-operations";
import { createThreadTitleService, type ThreadTitleService } from "../threads/workflows/thread-title-service";
import { isThreadsArchiveConfirmPointer, renderThreadsViewShell, unmountThreadsViewShell } from "./shell.dom";
import {
type ThreadsGeneratingRenameState,

View file

@ -6,8 +6,8 @@ import {
type ThreadRenameActiveState,
type ThreadRenameGeneratingState,
transitionThreadRenameLifecycleState,
} from "../threads/rename-lifecycle";
import { type ThreadRowCoreProjection, threadRowCoreProjection } from "../threads/row-projection";
} from "../threads/list/rename-lifecycle";
import { type ThreadRowCoreProjection, threadRowCoreProjection } from "../threads/list/row-projection";
type ThreadsLiveStatus = "pending" | "running" | "open";

View file

@ -1,5 +1,5 @@
import type { Thread } from "../../domain/threads/model";
import { threadDisplayTitle, threadRenameDraftTitle } from "../../domain/threads/title";
import type { Thread } from "../../../domain/threads/model";
import { threadDisplayTitle, threadRenameDraftTitle } from "../../../domain/threads/title";
import type { ThreadRenameActiveState } from "./rename-lifecycle";
interface ThreadRowCoreRenameProjection {

View file

@ -1,9 +1,9 @@
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 { ArchiveExportSettings } from "../../domain/threads/archive-markdown";
import { normalizeExplicitThreadName } from "../../domain/threads/model";
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 { ArchiveExportSettings } from "../../../domain/threads/archive-markdown";
import { normalizeExplicitThreadName } from "../../../domain/threads/model";
export interface ThreadOperationsHost {
clientAccess: AppServerClientAccess;

View file

@ -1,14 +1,14 @@
import type { AppServerClientAccess } from "../../app-server/connection/client-access";
import { generateThreadTitleWithCodex } from "../../app-server/services/thread-title-generation";
import { readCompletedConversationSummariesPage } from "../../app-server/services/threads";
import type { ReasoningEffort } from "../../domain/catalog/metadata";
import type { AppServerClientAccess } from "../../../app-server/connection/client-access";
import { generateThreadTitleWithCodex } from "../../../app-server/services/thread-title-generation";
import { readCompletedConversationSummariesPage } from "../../../app-server/services/threads";
import type { ReasoningEffort } from "../../../domain/catalog/metadata";
import {
findThreadTitleContext,
THREAD_TITLE_CONTEXT_UNAVAILABLE_MESSAGE,
type ThreadTitleContext,
threadTitleContextFromConversationSummary,
} from "../../domain/threads/title-generation-model";
import type { ThreadConversationSummary } from "../../domain/threads/transcript";
} from "../../../domain/threads/title-generation-model";
import type { ThreadConversationSummary } from "../../../domain/threads/transcript";
export interface ThreadTitleServiceHost {
codexPath: () => string;

View file

@ -11,7 +11,7 @@ import {
createAutoTitleCoordinator,
} from "../../../../../src/features/chat/application/threads/auto-title-coordinator";
import { threadTitleContextFromMessageStreamItems } from "../../../../../src/features/chat/application/threads/title-context";
import { createThreadTitleService } from "../../../../../src/features/threads/thread-title-service";
import { createThreadTitleService } from "../../../../../src/features/threads/workflows/thread-title-service";
import { DEFAULT_SETTINGS } from "../../../../../src/settings/model";
import { deferred } from "../../../../support/async";

View file

@ -5,7 +5,7 @@ import {
type ThreadRenameGeneratingState,
type ThreadRenameLifecycleState,
transitionThreadRenameLifecycleState,
} from "../../../src/features/threads/rename-lifecycle";
} from "../../../../src/features/threads/list/rename-lifecycle";
describe("thread rename lifecycle", () => {
it("keeps late generation callbacks scoped to the active unchanged generation", () => {

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { Thread } from "../../../src/domain/threads/model";
import { threadRowCoreProjection } from "../../../src/features/threads/row-projection";
import type { Thread } from "../../../../src/domain/threads/model";
import { threadRowCoreProjection } from "../../../../src/features/threads/list/row-projection";
describe("thread row core projection", () => {
it("projects shared row title, selection, rename, and archive confirmation state", () => {

View file

@ -1,16 +1,16 @@
import { describe, expect, it, vi } from "vitest";
import type { AppServerClient } from "../../../src/app-server/connection/client";
import type { ArchiveThreadResult } from "../../../src/app-server/services/thread-archive";
import type { ArchiveExportDestination } from "../../../src/app-server/services/thread-archive-markdown";
import { createThreadOperations, type ThreadOperationsHost } from "../../../src/features/threads/thread-operations";
import { DEFAULT_SETTINGS } from "../../../src/settings/model";
import type { AppServerClient } from "../../../../src/app-server/connection/client";
import type { ArchiveThreadResult } from "../../../../src/app-server/services/thread-archive";
import type { ArchiveExportDestination } from "../../../../src/app-server/services/thread-archive-markdown";
import { createThreadOperations, type ThreadOperationsHost } from "../../../../src/features/threads/workflows/thread-operations";
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
const archiveMock = vi.hoisted(() => ({
archiveThreadOnAppServer: vi.fn(),
}));
vi.mock("../../../src/app-server/services/thread-archive", () => ({
vi.mock("../../../../src/app-server/services/thread-archive", () => ({
archiveThreadOnAppServer: archiveMock.archiveThreadOnAppServer,
}));

View file

@ -1,12 +1,12 @@
import { describe, expect, it, vi } from "vitest";
import { THREAD_TITLE_CONTEXT_UNAVAILABLE_MESSAGE, type ThreadTitleContext } from "../../../src/domain/threads/title-generation-model";
import { THREAD_TITLE_CONTEXT_UNAVAILABLE_MESSAGE, type ThreadTitleContext } from "../../../../src/domain/threads/title-generation-model";
import {
createThreadTitleService,
type ThreadTitleService,
type ThreadTitleServiceHost,
} from "../../../src/features/threads/thread-title-service";
import { DEFAULT_SETTINGS } from "../../../src/settings/model";
} from "../../../../src/features/threads/workflows/thread-title-service";
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
describe("ThreadTitleService", () => {
it("generates a title from visible context without saving it", async () => {

View file

@ -7,7 +7,6 @@ import { describe, expect, it } from "vitest";
const repoRoot = process.cwd();
const biomeBin = path.join(repoRoot, "node_modules", ".bin", "biome");
const workspaceByPlugins = new Map();
const projectPluginByName = new Map(
parseJsonc(readFileSync(path.join(repoRoot, "biome.jsonc"), "utf8")).plugins.map((plugin) => {
const pluginPath = typeof plugin === "string" ? plugin : plugin.path;
@ -16,8 +15,8 @@ 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 RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE =
"Keep responsibility-split source roots free of module files; add modules to the matching subfolder instead of the root.";
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 =
@ -409,6 +408,7 @@ export function timestamp(): number {
"no-chat-workspace-boundary-imports.grit",
"no-feature-workspace-boundary-imports.grit",
"no-workspace-chat-internal-imports.grit",
"no-responsibility-root-module-files.grit",
"no-chat-host-rendering-layer-imports.grit",
"no-chat-panel-runtime-boundary-imports.grit",
"no-chat-presentation-outer-layer-imports.grit",
@ -531,6 +531,36 @@ import type { ChatWorkspacePanelSurface } from "../features/chat/host/contracts"
import type { CodexChatView } from "../features/chat/host/view.obsidian";
export type Allowed = ChatWorkspacePanelSurface | CodexChatView;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/domain/mixed-root.ts"),
`
export const misplaced = true;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/shared/mixed-root.ts"),
`
export const misplaced = true;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/mixed-root.ts"),
`
export const misplaced = true;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/threads/mixed-root.ts"),
`
export const misplaced = true;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/threads/list/rename-lifecycle.ts"),
`
export const allowed = true;
`.trimStart(),
);
await writeFile(
@ -615,6 +645,11 @@ export const value = statusText;
"src/features/threads-view/workspace-allowed.ts",
"src/workspace/chat-internal-escape.ts",
"src/workspace/chat-host-allowed.ts",
"src/domain/mixed-root.ts",
"src/shared/mixed-root.ts",
"src/features/chat/mixed-root.ts",
"src/features/threads/mixed-root.ts",
"src/features/threads/list/rename-lifecycle.ts",
"src/features/chat/panel/outer.tsx",
"src/features/chat/panel/allowed.tsx",
"src/features/chat/presentation/outer.ts",
@ -644,6 +679,11 @@ export const value = statusText;
expect(pluginDiagnostics(report, "src/features/threads-view/workspace-allowed.ts")).toEqual([]);
expect(pluginMessages(report, "src/workspace/chat-internal-escape.ts")).toEqual([WORKSPACE_CHAT_INTERNAL_MESSAGE]);
expect(pluginDiagnostics(report, "src/workspace/chat-host-allowed.ts")).toEqual([]);
expect(pluginMessages(report, "src/domain/mixed-root.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]);
expect(pluginMessages(report, "src/shared/mixed-root.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]);
expect(pluginMessages(report, "src/features/chat/mixed-root.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]);
expect(pluginMessages(report, "src/features/threads/mixed-root.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]);
expect(pluginDiagnostics(report, "src/features/threads/list/rename-lifecycle.ts")).toEqual([]);
expect(pluginMessages(report, "src/features/chat/panel/outer.tsx")).toEqual(
Array.from({ length: 3 }, () => CHAT_PANEL_RUNTIME_BOUNDARY_MESSAGE),
);
@ -787,7 +827,7 @@ 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-responsibility-root-module-files.grit",
"no-app-server-root-module-imports.grit",
"no-app-server-subfolder-root-imports.grit",
]);
@ -836,7 +876,7 @@ export const read = listThreads;
cwd,
);
expect(pluginMessages(report, "src/app-server/escape.ts")).toEqual([APP_SERVER_ROOT_MODULE_FILE_MESSAGE]);
expect(pluginMessages(report, "src/app-server/escape.ts")).toEqual([RESPONSIBILITY_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([]);
@ -1310,10 +1350,6 @@ export type AppServerThreadResumeClient = Pick<AppServerClient, "resumeThread">;
}
async function tempBiomeWorkspace(plugins) {
const cacheKey = plugins.join("\0");
const cached = workspaceByPlugins.get(cacheKey);
if (cached) return cached;
const cwd = await mkdtemp(path.join(tmpdir(), "codex-panel-grit-policy-"));
await mkdir(cwd, { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/domain/message-stream"), { recursive: true });
@ -1328,6 +1364,8 @@ async function tempBiomeWorkspace(plugins) {
await mkdir(path.join(cwd, "src/features/chat/presentation"), { recursive: true });
await mkdir(path.join(cwd, "src/features/chat/ui"), { recursive: true });
await mkdir(path.join(cwd, "src/features/selection-rewrite"), { recursive: true });
await mkdir(path.join(cwd, "src/features/threads/list"), { recursive: true });
await mkdir(path.join(cwd, "src/features/threads/workflows"), { recursive: true });
await mkdir(path.join(cwd, "src/features/threads-view"), { recursive: true });
await mkdir(path.join(cwd, "src/settings"), { recursive: true });
await mkdir(path.join(cwd, "src/domain/threads"), { recursive: true });
@ -1347,7 +1385,6 @@ async function tempBiomeWorkspace(plugins) {
css: { linter: { enabled: true } },
}),
);
workspaceByPlugins.set(cacheKey, cwd);
return cwd;
}