feat(tree): complete rust family runtime checklist

- add tree shell runtime artifact contracts and page/filetree/picker runtime reducers
- sink tree.subtree.move write operation through Rust and formalize command event plans
- harden file tree search projection contract and route thin-proxy boundaries
- record completed harness tasks and move design docs into process/done
This commit is contained in:
lix-2026
2026-04-27 10:27:15 +08:00
parent e564dfde02
commit 4ab36a9386
30 changed files with 2502 additions and 432 deletions
@@ -1,5 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchKernelFileTreeProjection } from "./projection-client";
import {
FILE_TREE_SEARCH_MAX_RESULTS,
buildFileTreeProjectionSearchRequestMeta,
fetchKernelFileTreeProjection,
} from "./projection-client";
describe("fetchKernelFileTreeProjection", () => {
beforeEach(() => {
@@ -42,6 +46,59 @@ describe("fetchKernelFileTreeProjection", () => {
expect(result.items.map((item) => item.rowId)).toEqual(["asset:table_1"]);
});
it("固定 file_tree 搜索语义边界:命中数先截断,祖先补全不计入 maxResults", () => {
expect(FILE_TREE_SEARCH_MAX_RESULTS).toBe(80);
expect(
buildFileTreeProjectionSearchRequestMeta({
workspaceId: "ws_1",
query: " rust ",
maxResults: 500,
}),
).toEqual({
query: "rust",
maxResults: 80,
maxResultsRule: "matches_only_before_ancestor_completion",
ancestorCompletion: "include_all_ancestors_after_match_truncation",
ordering: "kernel_file_tree_preorder",
emptyStateText: "没有匹配结果",
asyncVisibility: {
source: "kernel.project_view",
requestKey: "ws_1:rust",
},
coveredResourceKinds: ["index", "asset", "asset_folder", "mindmap", "book", "pdf"],
});
});
it("搜索请求会把 maxResults 限制在 sidebar 使用的稳定上限内", async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
ok: true,
result: {
projectionId: "kernel_projection:file_tree:root",
projection: "file_tree",
rootNodeId: null,
items: [],
edges: [],
},
}),
{ status: 200 },
),
);
vi.stubGlobal("fetch", fetchMock);
await fetchKernelFileTreeProjection({
workspaceId: "ws_1",
query: "rust",
maxResults: 500,
});
expect(fetchMock).toHaveBeenCalledWith(
"/api/tree/projections/file?workspaceId=ws_1&query=rust&maxResults=80",
expect.anything(),
);
});
it("失败时透出服务端错误消息", async () => {
vi.stubGlobal(
"fetch",
@@ -1,5 +1,22 @@
import type { KernelFileTreeProjection } from "@/lib/kernel-file-tree";
export const FILE_TREE_SEARCH_MAX_RESULTS = 80;
export const FILE_TREE_SEARCH_EMPTY_STATE_TEXT = "没有匹配结果";
export type FileTreeProjectionSearchRequestMeta = {
query: string | null;
maxResults: number | null;
maxResultsRule: "matches_only_before_ancestor_completion";
ancestorCompletion: "include_all_ancestors_after_match_truncation";
ordering: "kernel_file_tree_preorder";
emptyStateText: typeof FILE_TREE_SEARCH_EMPTY_STATE_TEXT;
asyncVisibility: {
source: "kernel.project_view";
requestKey: string | null;
};
coveredResourceKinds: ["index", "asset", "asset_folder", "mindmap", "book", "pdf"];
};
export type FetchKernelFileTreeProjectionInput = {
workspaceId: string;
rootNodeId?: string | null;
@@ -8,6 +25,38 @@ export type FetchKernelFileTreeProjectionInput = {
maxResults?: number | null;
};
function normalizeQuery(value: string | null | undefined): string | null {
const query = value?.trim() ?? "";
return query.length > 0 ? query : null;
}
function normalizeMaxResults(value: number | null | undefined): number | null {
if (typeof value !== "number" || !Number.isFinite(value)) {
return null;
}
return Math.min(FILE_TREE_SEARCH_MAX_RESULTS, Math.max(1, Math.floor(value)));
}
export function buildFileTreeProjectionSearchRequestMeta(
input: Pick<FetchKernelFileTreeProjectionInput, "workspaceId" | "query" | "maxResults">,
): FileTreeProjectionSearchRequestMeta {
const query = normalizeQuery(input.query);
const maxResults = normalizeMaxResults(input.maxResults);
return {
query,
maxResults,
maxResultsRule: "matches_only_before_ancestor_completion",
ancestorCompletion: "include_all_ancestors_after_match_truncation",
ordering: "kernel_file_tree_preorder",
emptyStateText: FILE_TREE_SEARCH_EMPTY_STATE_TEXT,
asyncVisibility: {
source: "kernel.project_view",
requestKey: query ? `${input.workspaceId}:${query}` : null,
},
coveredResourceKinds: ["index", "asset", "asset_folder", "mindmap", "book", "pdf"],
};
}
export async function fetchKernelFileTreeProjection(
input: FetchKernelFileTreeProjectionInput,
): Promise<KernelFileTreeProjection> {
@@ -20,12 +69,13 @@ export async function fetchKernelFileTreeProjection(
if (typeof input.depth === "number" && Number.isFinite(input.depth)) {
params.set("depth", String(input.depth));
}
const query = input.query?.trim();
const query = normalizeQuery(input.query);
if (query) {
params.set("query", query);
}
if (typeof input.maxResults === "number" && Number.isFinite(input.maxResults)) {
params.set("maxResults", String(Math.max(1, Math.floor(input.maxResults))));
const maxResults = normalizeMaxResults(input.maxResults);
if (maxResults != null) {
params.set("maxResults", String(maxResults));
}
const response = await fetch(`/api/tree/projections/file?${params.toString()}`, {