dsh-codegraph-visualizer 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +391 -226
- package/assets/icon-180.png +0 -0
- package/assets/icon.svg +30 -0
- package/cordis.patch.yml +22 -17
- package/dist/CodeGraphAdapter-BNIK0xHU.js +422 -0
- package/dist/CodeGraphAdapter-BNIK0xHU.js.map +1 -0
- package/dist/CodeGraphAdapter-CXj1hbdm.js +10 -0
- package/dist/FileSystemAdapter-CgasSGbB.js +524 -0
- package/dist/FileSystemAdapter-CgasSGbB.js.map +1 -0
- package/dist/LensAdapter-C9Saf_i_.js +89 -0
- package/dist/LensAdapter-C9Saf_i_.js.map +1 -0
- package/dist/Logger-DUIJSeNu.js +80 -0
- package/dist/Logger-DUIJSeNu.js.map +1 -0
- package/dist/TreeSitterAdapter-C-EWy8br.js +631 -0
- package/dist/TreeSitterAdapter-C-EWy8br.js.map +1 -0
- package/dist/TreeSitterAdapter-C6FcO2_s.js +8 -0
- package/dist/client/index.css +8963 -1724
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.d.ts +634 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.esm.js +57197 -20713
- package/dist/client/index.js.map +1 -0
- package/dist/client/{index.js → worker/layoutWorker.js} +7657 -8787
- package/dist/client.js +90426 -0
- package/dist/codeGraphDb-B-vpZmpm.js +9 -0
- package/dist/codeGraphDb-T6K3dK5D.js +357 -0
- package/dist/codeGraphDb-T6K3dK5D.js.map +1 -0
- package/dist/codeGraphRows-BhZX34uB.js +6 -0
- package/dist/codeGraphRows-C7hqu35E.js +246 -0
- package/dist/codeGraphRows-C7hqu35E.js.map +1 -0
- package/dist/constants-CQq93l3e.js +399 -0
- package/dist/constants-CQq93l3e.js.map +1 -0
- package/dist/graphFingerprint-CeARo7jG.js +4 -0
- package/dist/graphFingerprint-PuxORZUF.js +277 -0
- package/dist/graphFingerprint-PuxORZUF.js.map +1 -0
- package/dist/graphMetrics-C0F30730.js +555 -0
- package/dist/graphMetrics-C0F30730.js.map +1 -0
- package/dist/index.d.ts +572 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14819 -2009
- package/dist/index.js.map +1 -1
- package/dist/inheritanceEnhancer-Bi3UoYJS.js +291 -0
- package/dist/inheritanceEnhancer-Bi3UoYJS.js.map +1 -0
- package/dist/layerUtils-DDm-EsaD.js +72 -0
- package/dist/layerUtils-DDm-EsaD.js.map +1 -0
- package/dist/nativeIndex-CABbgERW.js +347 -0
- package/dist/nativeIndex-CABbgERW.js.map +1 -0
- package/dist/nativeIndex-esaDOsq8.js +5 -0
- package/dist/treeSitterExtractors-B1ybKNrC.js +394 -0
- package/dist/treeSitterExtractors-B1ybKNrC.js.map +1 -0
- package/dist/types-BUDxI_00.js +23 -0
- package/dist/types-BUDxI_00.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh-CN.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +100 -20
- package/dist/client-cjs/index.cjs +0 -44283
- package/dist/client-cjs/index.css +0 -2404
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants-CQq93l3e.js","names":["TOOL_MESSAGES: Record<string, { en: string; zh: string }>","ERROR_MESSAGES: Record<string, { en: string; zh: string }>"],"sources":["../src/shared/constants.ts"],"sourcesContent":["export const RENDERER = {\r\n MIN_ZOOM: 0.02,\r\n MAX_ZOOM: 3,\r\n FIT_PADDING: 80,\r\n FIT_DELAY_SMALL: 200,\r\n FIT_DELAY_LARGE: 500,\r\n LARGE_GRAPH_NODE_THRESHOLD: 500,\r\n LAYOUT_TIMEOUT_MS: 10000,\r\n WORKER_NODE_THRESHOLD: 500,\r\n // A worker that has never answered gets a shorter budget than one already proven responsive:\r\n // the first round trip also pays for fetching and compiling the worker bundle, and a silent\r\n // worker must not keep a layout blocked for the full LAYOUT_TIMEOUT_MS.\r\n WORKER_HANDSHAKE_TIMEOUT_MS: 5000,\r\n // Hard cap on worker (re)construction attempts. `new Worker(url)` never throws for an\r\n // unreachable URL — the failure arrives later as an async `error` event — so without a cap the\r\n // error handler would rebuild forever and hammer the server with 404s.\r\n WORKER_MAX_INIT_RETRIES: 3,\r\n INCREMENTAL_LAYOUT_THRESHOLD: 50,\r\n INCREMENTAL_LAYOUT_DELAY_MS: 50,\r\n FULL_LAYOUT_DELAY_MS: 150,\r\n FOCUS_NODE_ANIMATION_MS: 300,\r\n NEIGHBORHOOD_ANIMATION_MS: 400,\r\n DEFAULT_CONTAINER_WIDTH: 800,\r\n DEFAULT_CONTAINER_HEIGHT: 600,\r\n} as const;\r\n\r\n/**\r\n * The layout worker has to be fetched over HTTP because a browser Worker cannot be given an\r\n * in-memory module. The plugin's own host serves it, so the client and the host share this one\r\n * path: the client builds the Worker URL from it and the host registers the route for it.\r\n *\r\n * `ARTIFACT` is the file the build produces; `tests/unit/workerContract.test.ts` keeps the two in\r\n * step, because a route that serves nothing (or a client that asks for a path nobody registered)\r\n * is silent at runtime: layouts just fall back to the main thread.\r\n */\r\nexport const LAYOUT_WORKER = {\r\n ROUTE_PATH: '/api/v1/codegraph/worker/layoutWorker.js',\r\n ARTIFACT: 'dist/client/worker/layoutWorker.js',\r\n SOURCE_ENTRY: 'src/client/worker/layoutWorker.ts',\r\n} as const;\r\n\r\nexport const CLUSTER = {\r\n MAX_VISIBLE_NODES: 1500,\r\n SUBGROUP_THRESHOLD: 100,\r\n MAX_NODES_PER_DIR_BEFORE_SUBGROUP: 500,\r\n LARGE_NODE_THRESHOLD: 2000,\r\n MIN_NODES_FOR_SUBGROUP: 50,\r\n MAX_ITERATIONS_LARGE: 3,\r\n MAX_ITERATIONS_SMALL: 10,\r\n MAX_NODES_FOR_LABEL_PROP: 5000,\r\n EXPAND_BUDGET_SINGLE: 300,\r\n} as const;\r\n\r\nexport const DYNAMIC_CLUSTER = {\r\n SMALL_SCREEN_WIDTH: 1200,\r\n LARGE_SCREEN_WIDTH: 1920,\r\n MAX_VISIBLE_NODES_SMALL: 800,\r\n MAX_VISIBLE_NODES_MEDIUM: 1200,\r\n MAX_VISIBLE_NODES_LARGE: 1500,\r\n AUTO_DEPTH_THRESHOLD_1: 3000,\r\n AUTO_DEPTH_THRESHOLD_2: 10000,\r\n} as const;\r\n\r\nexport function getDynamicMaxVisibleNodes(viewportWidth: number): number {\r\n if (viewportWidth < DYNAMIC_CLUSTER.SMALL_SCREEN_WIDTH) {\r\n return DYNAMIC_CLUSTER.MAX_VISIBLE_NODES_SMALL;\r\n }\r\n if (viewportWidth >= DYNAMIC_CLUSTER.LARGE_SCREEN_WIDTH) {\r\n return DYNAMIC_CLUSTER.MAX_VISIBLE_NODES_LARGE;\r\n }\r\n return DYNAMIC_CLUSTER.MAX_VISIBLE_NODES_MEDIUM;\r\n}\r\n\r\nexport function getAutoClusterDepth(nodeCount: number, maxVisibleNodes: number): number {\r\n if (nodeCount > maxVisibleNodes * 5) return 2;\r\n if (nodeCount > maxVisibleNodes * 1.5) return 1;\r\n return 0;\r\n}\r\n\r\nexport const PATH_FINDER = {\r\n\r\n MAX_NODES_FOR_BFS: 50000,\r\n} as const;\r\n\r\nexport const HIGHLIGHT = {\r\n CALL_CHAIN_MAX_DEPTH: 50,\r\n IMPACT_MAX_DEPTH: 3,\r\n} as const;\r\n\r\nexport const HISTORY = {\r\n DEFAULT_MAX_HISTORY: 50,\r\n} as const;\r\n\r\nexport const POLLING = {\r\n PREREQ_INITIAL_DELAY_MS: 3000,\r\n PREREQ_INTERVAL_MS: 30000,\r\n WORKSPACE_INTERVAL_MS: 15000,\r\n LOADING_FAILSAFE_MS: 15000,\r\n WORKSPACE_SCAN_DEBOUNCE_MS: 300,\r\n PREREQ_CHECK_TIMEOUT_MS: 3000,\r\n WORKSPACE_TICK_MS: 30000,\r\n} as const;\r\n\r\nexport const API = {\r\n DEFAULT_TIMEOUT_MS: 30000,\r\n RATE_LIMIT_WINDOW_MS: 60000,\r\n RATE_LIMIT_DEFAULT: 60,\r\n RATE_LIMIT_HEAVY: 10,\r\n RATE_LIMIT_MAP_MAX: 10000,\r\n SCAN_TIMEOUT_MS: 60000,\r\n DATA_RESPONSE_MAX_BYTES: 5 * 1024 * 1024,\r\n DATA_RESPONSE_SLIM_NODES: 500,\r\n INVOKE_DEFAULT_TIMEOUT_MS: 5000,\r\n} as const;\r\n\r\nexport const HEALTH_SCORE = {\r\n WEIGHT_COUPLING: 0.25,\r\n WEIGHT_COHESION: 0.25,\r\n WEIGHT_CYCLIC: 0.30,\r\n WEIGHT_MODULARITY: 0.20,\r\n DEFAULT_MAX_MODULE_DEPTH: 3,\r\n} as const;\r\n\r\nexport const LAYOUT = {\r\n DENSITY_FACTOR_SMALL: 1.5,\r\n DENSITY_FACTOR_MEDIUM: 1.0,\r\n DENSITY_FACTOR_LARGE: 0.7,\r\n SMALL_NODE_THRESHOLD: 50,\r\n MEDIUM_NODE_THRESHOLD: 200,\r\n LARGE_NODE_THRESHOLD: 500,\r\n VERY_LARGE_NODE_THRESHOLD: 2000,\r\n LAYOUT_SAMPLE_SIZE: 200,\r\n COSE_NUM_ITER: 2500,\r\n COSE_LARGE_NUM_ITER: 15000,\r\n MAX_RANK_SEP: 80,\r\n MIN_RANK_SEP: 30,\r\n MIN_NODE_SPACING: 20,\r\n SMALL_SPACING_FACTOR: 1.2,\r\n MEDIUM_SPACING_FACTOR: 1.0,\r\n LARGE_SPACING_FACTOR: 0.8,\r\n SMALL_NODE_COUNT_THRESHOLD: 100,\r\n MEDIUM_NODE_COUNT_THRESHOLD: 300,\r\n INCREMENTAL_RADIUS_BASE: 100,\r\n GRID_FALLBACK_NODE_THRESHOLD: 5000,\r\n OVERLAP_MAX_ITERATIONS: 15,\r\n OVERLAP_SKIP_THRESHOLD: 5000,\r\n OVERLAP_CELL_SIZE_MULTIPLIER: 4,\r\n GOLDEN_ANGLE: Math.PI * (3 - Math.sqrt(5)),\r\n OVERLAP_PUSH_COEFFICIENT: 0.15,\r\n OVERLAP_MIN_PUSH: 2,\r\n FCOSE_LARGE_NODE_THRESHOLD: 5000,\r\n FCOSE_LARGE_NUM_ITER: 1000,\r\n FCOSE_LARGE_QUALITY: 'draft' as const,\r\n NODE_REPULSE_MAX: 20000,\r\n} as const;\r\n\r\nexport const VIEWPORT = {\r\n THUMBNAIL_TARGET_WIDTH: 200,\r\n THUMBNAIL_MIN_SCALE: 0.05,\r\n THUMBNAIL_MAX_SCALE: 0.3,\r\n} as const;\r\n\r\n/**\r\n * 虚拟视口(视口外节点不渲染)的开关阈值与视口外扩边距。\r\n *\r\n * ## SCROLL_THRESHOLD 必须低于聚类上限(cycle-93 的联合校准)\r\n *\r\n * 这个阈值单独看很合理,但它和 `CLUSTER` / `DYNAMIC_CLUSTER` 是**一对**约束:\r\n * 虚拟视口只在**显示层**节点数超过阈值时启用(唯一消费点\r\n * `src/client/hooks/useGraphRenderer.ts:341-349`),而显示层在 `code` 视图下由\r\n * `computeProgressiveClusters` 产出、通常就是「一级目录聚类」的个数\r\n * (`progressiveCluster.ts:448-503`:depth 0 时每个目录一个聚类节点)。\r\n *\r\n * 历史值 2500 是错的:一级目录数几乎不可能超过 2500,于是**默认视图下这个特性\r\n * 永远不启用**(与 cycle-88「ViewLayer 第三个成员可读不可达」同族——代码在、\r\n * 测试绿、功能死)。两个常量在 git 历史上从未联合校准过:\r\n * `VIRTUAL_VIEWPORT` 是 2000 → 5000 → 2500 独立演进,`MAX_VISIBLE_NODES` 由另一次\r\n * 提交引入。\r\n *\r\n * 取 500 的理由:\r\n * - 必须**小于**最小动态上限 `DYNAMIC_CLUSTER.MAX_VISIBLE_NODES_SMALL`(800)\r\n * 与 `CLUSTER.MAX_VISIBLE_NODES`(1500):凡是会被 `enforceNodeLimit` 折叠的视图,\r\n * 显示层永远到不了 2500;\r\n * - 500 是本仓库既有的「大图」分界线(`LAYOUT.LARGE_NODE_THRESHOLD`、\r\n * `WORKER_NODE_THRESHOLD` 都是 500),三处口径一致。\r\n *\r\n * 需要说清的一点:`enforceNodeLimit` 只折叠**聚类节点**,对大量普通节点\r\n * (例如几千个一级目录、或分层视图的 layer0)并不构成硬上限——所以「阈值低于上限」\r\n * 只是必要条件。真正被 J35 依赖的是**分层视图**这条无上限路径\r\n * (`graphDataSlice.ts:65-101` / `viewModeTransition.ts:108-122` 直写,\r\n * 层定义见 `diagram/index.ts:63-114`)。\r\n *\r\n * 两条约束由 `tests/unit/virtualScrollReachability.test.ts` 钉住:任何一方被改动\r\n * 到使阈值不可达,该测试立刻变红。\r\n */\r\nexport const VIRTUAL_VIEWPORT = {\r\n SCROLL_THRESHOLD: 500,\r\n PADDING: 100,\r\n} as const;\r\n\r\nexport const QUADTREE = {\r\n MAX_NODES_PER_QUAD: 10,\r\n MAX_DEPTH: 8,\r\n INITIAL_BOUNDS: { x1: -100000, y1: -100000, x2: 100000, y2: 100000 },\r\n EXPAND_PADDING: 5000,\r\n} as const;\r\n\r\nexport const EXPORT = {\r\n SVG_PADDING: 160,\r\n MERMAID_MAX_NODES: 500,\r\n MERMAID_MAX_EDGES: 800,\r\n MERMAID_MAX_SEQUENCE_MESSAGES: 100,\r\n} as const;\r\n\r\nexport const FILTER = {\r\n MAX_PATTERN_LENGTH: 200,\r\n MAX_NESTED_QUANTIFIER_DEPTH: 2,\r\n} as const;\r\n\r\nexport const DATA_CONTROLLER = {\r\n BATCH_SIZE: 200,\r\n SVG_OFFSET: 80,\r\n MAX_WEIGHT: 10,\r\n HUB_DEGREE_THRESHOLD: 5,\r\n} as const;\r\n\r\nexport const LOD = {\r\n MINIMAL_THRESHOLD_SMALL: 0.15,\r\n MINIMAL_THRESHOLD_MEDIUM: 0.25,\r\n MINIMAL_THRESHOLD_LARGE: 0.35,\r\n COMPACT_THRESHOLD_SMALL: 0.4,\r\n COMPACT_THRESHOLD_MEDIUM: 0.55,\r\n COMPACT_THRESHOLD_LARGE: 0.7,\r\n SMALL_NODE_COUNT: 100,\r\n MEDIUM_NODE_COUNT: 500,\r\n MINIMAL_NODE_SIZE: 8,\r\n COMPACT_NODE_SIZE: 16,\r\n MINIMAL_EDGE_WIDTH: 0.5,\r\n COMPACT_EDGE_WIDTH: 1,\r\n COMPACT_FONT_SIZE: 6,\r\n TRANSITION_DURATION_MS: 200,\r\n MINIMAL_CLUSTER_DEPTH: 0,\r\n COMPACT_CLUSTER_DEPTH: 1,\r\n} as const;\r\n\r\nexport const HEATMAP = {\r\n LINES_LOW: 50,\r\n LINES_HIGH: 500,\r\n COMPLEXITY_LOW: 5,\r\n COMPLEXITY_HIGH: 20,\r\n COLOR_STABLE: '#10b981',\r\n COLOR_UNSTABLE: '#ef4444',\r\n COLOR_LOW: '#3b82f6',\r\n COLOR_HIGH: '#ef4444',\r\n} as const;\r\n\r\nexport const TODO_MARKER = {\r\n COLOR_TODO: '#f59e0b',\r\n COLOR_FIXME: '#ef4444',\r\n COLOR_BOTH: '#dc2626',\r\n BORDER_WIDTH: 3,\r\n BORDER_STYLE: 'double',\r\n} as const;\r\n\r\n\r\nexport const CYCLE_DETECTION = {\r\n MAX_NODES_FOR_DETECTION: 10000,\r\n} as const;\r\n\r\nexport const INCREMENTAL_CLUSTER = {\r\n MAX_CHANGE_RATIO: 0.3,\r\n MIN_PREV_NODES: 100,\r\n} as const;\r\n\r\nexport const VIEW_MODE = {\r\n DEPTH_FILTER_LEVEL_1: 3,\r\n} as const;\r\nexport const CORE_FUNCTION = {\r\n MIN_INDEGREE_HUB: 5,\r\n MIN_OUTDEGREE_HUB: 5,\r\n MIN_INFLUENCE_REACH: 10,\r\n MIN_CALL_CHAIN_DEPTH: 3,\r\n MAX_BFS_NODES: 500,\r\n} as const;\r\nexport const MCP = {\r\n MAX_MESSAGE_LENGTH: 10 * 1024 * 1024,\r\n MAX_IMPACT_RESULTS: 500,\r\n} as const;\r\n\r\nexport const CACHE = {\r\n GRAPH_CACHE_LIMIT: 8,\r\n} as const;\r\n\r\nexport const PREFETCH = {\r\n CONCURRENCY_LIMIT: 2,\r\n COST_THRESHOLD: 50,\r\n HOVER_DEBOUNCE_MS: 300,\r\n IDLE_TIMEOUT_MS: 2000,\r\n} as const;\r\n\r\nexport const RESTORE = {\r\n STEP_DELAY_MS: 50,\r\n} as const;\r\n\r\nexport const BOOTSTRAP = {\r\n TOP_LEVEL_NODE_LIMIT: 50,\r\n CHILDREN_PAGE_SIZE: 50,\r\n} as const;\r\n\r\nexport const FUNCTION_BODY = {\r\n MAX_EXPANDED: 2,\r\n} as const;\r\n\r\nexport const SYMBOL_AST = {\r\n MAX_NODES: 800,\r\n MAX_SOURCE_LINES: 1500,\r\n} as const;\r\n\r\n/**\r\n * 关键词图检索的参数边界(cycle-120 / graphify 关键词检索产品化)。\r\n *\r\n * **单一来源**:宿主 `/query` 路由的 `QUERY`(`src/host/routes/queryRoute.ts`)负责\r\n * 服务端校验,客户端面板的阈值控件负责输入夹紧——两端各自手写数字会静默分叉\r\n * (本仓反复的「两份顺序/枚举表必然漂移」教训)。客户端**只读这里的值**做 min/max\r\n * 夹紧与占位显示,越界仍由宿主 400 兜底(客户端夹紧是 UX,服务端校验是契约)。\r\n */\r\nexport const KEYWORD_QUERY_LIMITS = {\r\n /** 展开跳数上界(与宿主 QUERY.MAX_DEPTH 一致)。 */\r\n MAX_DEPTH: 5,\r\n /** 展开跳数缺省(与宿主 QUERY.DEFAULT_DEPTH 一致)。 */\r\n DEFAULT_DEPTH: 2,\r\n /** token 预算下界(宿主低于此值回 400)。 */\r\n MIN_TOKEN_BUDGET: 50,\r\n /** token 预算上界(与宿主 QUERY.MAX_TOKEN_BUDGET 一致)。 */\r\n MAX_TOKEN_BUDGET: 4000,\r\n /** token 预算缺省(与宿主 QUERY.DEFAULT_TOKEN_BUDGET 一致)。 */\r\n DEFAULT_TOKEN_BUDGET: 800,\r\n} as const;\r\n\r\nexport const FILE_OUTLINE = {\r\n MAX_SYMBOLS: 800,\r\n} as const;\r\n\r\nexport const SQLITE = {\r\n MAX_IN_PARAMS: 900,\r\n} as const;\r\n\r\nexport const EDGES_AGGREGATION = {\r\n THRESHOLD: 20,\r\n PAGE_SIZE: 500,\r\n} as const;\r\n\r\n/**\r\n * Client-side budget for the per-node edge list in the detail panel.\r\n * The server caps `limit` at `EDGES_AGGREGATION.PAGE_SIZE`, so PAGE_SIZE here\r\n * must stay at or below that value.\r\n */\r\nexport const EDGE_DETAIL = {\r\n /** Edges requested per page by the node-detail edge list. */\r\n PAGE_SIZE: 100,\r\n /** Edges rendered inline before the \"show more\" affordance appears. */\r\n PREVIEW_LIMIT: 5,\r\n} as const;\r\n\r\nexport const VALIDATION = {\r\n MAX_PATH_LENGTH: 4096,\r\n MAX_GIT_REF_LENGTH: 256,\r\n MAX_FILEPATH_LENGTH: 512,\r\n} as const;\r\n\r\nexport const AST = {\r\n MAX_CONTENT_BYTES: 512 * 1024,\r\n MAX_TREE_SITTER_CONTENT_BYTES: 512 * 1024,\r\n} as const;\r\n\r\nexport const WATCHER = {\r\n MAX_RETRIES: 3,\r\n RETRY_BASE_DELAY_MS: 1000,\r\n} as const;\r\n\r\n/** Structural-fingerprint incremental detection (P0-6). Bump VERSION when the canonical record\r\n * shape changes, so previously stored snapshot hashes are not mistaken for current ones. */\r\nexport const FINGERPRINT = {\r\n VERSION: 1,\r\n /** Snapshots retained per repo; older rows are pruned after each save. */\r\n SNAPSHOT_KEEP: 3,\r\n} as const;\r\n\r\n/** Targeted (file-scoped) graph refresh driven by the watcher (P2-21). */\r\nexport const INCREMENTAL = {\r\n /**\r\n * Watcher-supplied file paths accepted per update. Beyond this the targeted read stops being\r\n * cheaper than the full one, so callers fall back to a whole-graph fetch. The watcher already\r\n * clears its own tracking set past `MAX_CHANGED_FILES_TRACK`, which is why this ceiling is lower.\r\n */\r\n MAX_CHANGED_FILES: 50,\r\n /** Rows per `IN (...)` batch when reading the changed files back out of the index. */\r\n MAX_FILE_BATCH: 200,\r\n} as const;\r\n\r\n/**\r\n * `/manage/metrics` 的采样边界(cycle-141,P0 可用性)。\r\n *\r\n * ## 节点与边必须**各自**设上界\r\n *\r\n * 旧实现的循环条件只界节点(`allDbNodes.length < 10000`),边侧完全没有界:只要节点数\r\n * 没到上限,循环就把**全库边**读进内存。代码图谱的边数通常是节点数的 2–5 倍,一个\r\n * 9 千节点 / 4 万边的仓库就足以让这条路由 OOM。节点与边是**同一处内存护栏的两半**,\r\n * 缺一即回到旧缺陷——所以这里两个上界缺一不可。\r\n *\r\n * ## 边界只影响「读多少」,不影响「算得对不对」\r\n *\r\n * 触到上界时路由如实披露 `truncated: true` 与 `nodesTotal/edgesTotal`,客户端据此知道\r\n * 这不是全量图。上界不是精度承诺,是可用性护栏。\r\n */\r\nexport const METRICS = {\r\n MAX_NODES: 10000,\r\n MAX_EDGES: 50000,\r\n PAGE_SIZE: 500,\r\n} as const;\r\n\r\nexport const UI = {\r\n SEARCH_DEBOUNCE_MS: 200,\r\n RESIZE_DEBOUNCE_MS: 200,\r\n ANCESTOR_MIN_HEIGHT_PX: 300,\r\n ANCESTOR_SEARCH_MAX_DEPTH: 8,\r\n\r\n PANEL_MIN_SIZE: {\r\n 'better-sidebar': { width: 200, height: 180 },\r\n 'sidebar-right': { width: 200, height: 180 },\r\n slot: { width: 220, height: 200 },\r\n floating: { width: 320, height: 240 },\r\n 'conversation-view': { width: 280, height: 200 },\r\n unknown: { width: 320, height: 240 },\r\n } as const,\r\n TOOLBAR_BREAKPOINT_COMPACT_PX: 768,\r\n TOOLBAR_BREAKPOINT_NARROW_PX: 480,\r\n POLLING_INTERVAL_WATCH_MS: 10000,\r\n POLLING_INTERVAL_IDLE_MS: 30000,\r\n MINIMAP_UPDATE_INTERVAL_MS: 500,\r\n MINIMAP_THROTTLE_MS: 500,\r\n COPY_HINT_TIMEOUT_MS: 2000,\r\n INSTALL_HINT_TIMEOUT_MS: 3000,\r\n COPY_FEEDBACK_MS: 1600,\r\n IMPORT_CLOSE_DELAY_MS: 700,\r\n INIT_RETRY_DELAY_MS: 50,\r\n URL_REVOKE_DELAY_MS: 1000,\r\n CONTEXT_MENU_Z_INDEX: 10000,\r\n WORKSPACE_LIST_MAX: 10,\r\n MAX_FUNCTIONS_PER_CLUSTER: 50,\r\n} as const;\r\n\r\n/**\r\n * 本插件在客户端可用的集成通道。\r\n *\r\n * `remote-web-ui` 成员已在 cycle-83 删除:`ctx.get('remoteWebUI')` 在任何 DSH 代都不存在\r\n * (幻影),真实的 `@linxin666/dsh-remote-web-ui` 只写 DOM 适配标记、不提供任何面板 API,\r\n * 因此「远程窄屏」是版面适配维度而非集成通道,由\r\n * `src/client/compat/PlatformProbe.ts` 的 `isRemoteNarrow` 表达。\r\n */\r\nexport type IntegrationMode = 'better-sidebar' | 'slot' | 'floating' | 'unknown' | 'sidebar-right' | 'conversation-view';\r\n\r\nexport function getMinPanelSize(mode: IntegrationMode): { minWidth: number; minHeight: number } {\r\n const cfg = UI.PANEL_MIN_SIZE[mode] ?? UI.PANEL_MIN_SIZE.unknown;\r\n return { minWidth: cfg.width, minHeight: cfg.height };\r\n}\r\n\r\n/**\r\n * Integration modes where the host container owns the panel geometry.\r\n *\r\n * `src/client/styles/responsive.css` declares `width: 100%; height: 100%` for exactly\r\n * these modes, so the panel must track the DSH slot it was mounted into. A persisted\r\n * free-float size applied as an inline style would outrank that stylesheet and squeeze\r\n * the primary tab view down to the last floating size, so both the size applier and the\r\n * drag handle are disabled for them.\r\n */\r\nconst HOST_SIZED_INTEGRATION_MODES: ReadonlySet<IntegrationMode> = new Set<IntegrationMode>([\r\n 'conversation-view',\r\n 'sidebar-right',\r\n 'better-sidebar',\r\n]);\r\n\r\nexport function isHostSizedIntegrationMode(mode: IntegrationMode): boolean {\r\n return HOST_SIZED_INTEGRATION_MODES.has(mode);\r\n}\r\n\r\nexport const COLORS = {\r\n PACKAGE_LIGHT: '#263148',\r\n PACKAGE_DARK: '#232324',\r\n NODE_CLASS_LIGHT: '#1a2230',\r\n} as const;\r\nexport const TOOL_MESSAGES: Record<string, { en: string; zh: string }> = {\r\n NO_PATH_FOUND: { en: 'No path found between the specified symbols.', zh: '未找到指定符号之间的路径。' },\r\n PATH_PATTERN_FAILED: { en: 'Path pattern evaluation failed.', zh: '路径模式求值失败。' },\r\n GRAPH_TOO_LARGE_FOR_PATH: { en: 'Graph too large (>50000 nodes) for path finding. Try filtering first.', zh: '图谱过大(>50000 节点),无法进行路径查找。请先尝试筛选。' },\r\n PATH_FOUND: { en: 'Path found', zh: '路径已找到' },\r\n HOPS: { en: 'hops', zh: '跳' },\r\n INSUFFICIENT_DATA: { en: 'Insufficient data for health scoring.', zh: '数据不足,无法进行健康评分。' },\r\n NO_VIOLATIONS: { en: 'No rule violations detected.', zh: '未检测到规则违规。' },\r\n RULE_VIOLATIONS: { en: 'rule violations:', zh: '条规则违规:' },\r\n NO_CYCLES: { en: 'No dependency cycles detected.', zh: '未检测到依赖环。' },\r\n};\r\nexport const ERROR_CODES = {\r\n PATH_NOT_ALLOWED: 'PATH_NOT_ALLOWED',\r\n WORKSPACE_PATH_NOT_ALLOWED: 'WORKSPACE_PATH_NOT_ALLOWED',\r\n SYMLINK_ESCAPE: 'SYMLINK_ESCAPE',\r\n NO_WORKSPACE_PATH: 'NO_WORKSPACE_PATH',\r\n PATH_OUTSIDE_WORKSPACE: 'PATH_OUTSIDE_WORKSPACE',\r\n SCAN_FAILED: 'SCAN_FAILED',\r\n INIT_FAILED: 'INIT_FAILED',\r\n WATCH_TOGGLE_FAILED: 'WATCH_TOGGLE_FAILED',\r\n WATCH_SETUP_FAILED: 'WATCH_SETUP_FAILED',\r\n INTERNAL_ERROR: 'INTERNAL_ERROR',\r\n COMMITS_REQUIRED: 'COMMITS_REQUIRED',\r\n INVALID_GIT_REF: 'INVALID_GIT_REF',\r\n GRAPH_DIFF_FAILED: 'GRAPH_DIFF_FAILED',\r\n FILEPATH_REQUIRED: 'FILEPATH_REQUIRED',\r\n FILE_TOO_LARGE: 'FILE_TOO_LARGE',\r\n AST_PARSE_FAILED: 'AST_PARSE_FAILED',\r\n FILEPATH_AND_CONTENT_REQUIRED: 'FILEPATH_AND_CONTENT_REQUIRED',\r\n INVALID_FILEPATH: 'INVALID_FILEPATH',\r\n CONTENT_TOO_LARGE: 'CONTENT_TOO_LARGE',\r\n METHOD_NOT_ALLOWED: 'METHOD_NOT_ALLOWED',\r\n RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',\r\n SYMBOL_NOT_FOUND: 'SYMBOL_NOT_FOUND',\r\n BODY_TOO_LARGE: 'BODY_TOO_LARGE',\r\n INVALID_JSON_BODY: 'INVALID_JSON_BODY',\r\n INVALID_CONFIG: 'INVALID_CONFIG',\r\n INDEX_NOT_FOUND: 'INDEX_NOT_FOUND',\r\n DELETE_FAILED: 'DELETE_FAILED',\r\n METRICS_FAILED: 'METRICS_FAILED',\r\n SYNC_FAILED: 'SYNC_FAILED',\r\n} as const;\r\n\r\nexport const ERROR_MESSAGES: Record<string, { en: string; zh: string }> = {\r\n PATH_NOT_ALLOWED: { en: 'Path not allowed', zh: '路径不允许访问' },\r\n WORKSPACE_PATH_NOT_ALLOWED: { en: 'Workspace path not allowed', zh: '工作区路径不允许访问' },\r\n SYMLINK_ESCAPE: { en: 'Access denied: symlink escape', zh: '访问被拒绝:符号链接逃逸' },\r\n NO_WORKSPACE_PATH: { en: 'No workspace path configured', zh: '未配置工作区路径' },\r\n PATH_OUTSIDE_WORKSPACE: { en: 'Access denied: path outside workspace', zh: '访问被拒绝:路径超出工作区范围' },\r\n SCAN_FAILED: { en: 'Scan failed — check codegraph installation', zh: '扫描失败 — 请检查 codegraph 安装' },\r\n INIT_FAILED: { en: 'Initialization failed — is dsh-codegraph installed?', zh: '初始化失败 — dsh-codegraph 是否已安装?' },\r\n WATCH_TOGGLE_FAILED: { en: 'Watch toggle failed', zh: '监听切换失败' },\r\n WATCH_SETUP_FAILED: { en: 'Watch setup failed after retries', zh: '监听设置在重试后失败' },\r\n INTERNAL_ERROR: { en: 'Internal server error', zh: '服务器内部错误' },\r\n COMMITS_REQUIRED: { en: 'from and to commits are required', zh: '需要 from 和 to 提交参数' },\r\n INVALID_GIT_REF: { en: 'Invalid git ref format', zh: '无效的 Git 引用格式' },\r\n GRAPH_DIFF_FAILED: { en: 'Graph diff failed — check git refs', zh: '图谱差异计算失败 — 请检查 Git 引用' },\r\n FILEPATH_REQUIRED: { en: 'filePath is required', zh: 'filePath 参数为必填' },\r\n FILE_TOO_LARGE: { en: 'File too large for AST parsing', zh: '文件过大,无法进行 AST 解析' },\r\n AST_PARSE_FAILED: { en: 'AST parsing failed', zh: 'AST 解析失败' },\r\n FILEPATH_AND_CONTENT_REQUIRED: { en: 'filePath and content are required', zh: 'filePath 和 content 参数为必填' },\r\n INVALID_FILEPATH: { en: 'Invalid filePath', zh: '无效的 filePath' },\r\n CONTENT_TOO_LARGE: { en: 'Content too large for tree-sitter parsing', zh: '内容过大,无法进行 tree-sitter 解析' },\r\n METHOD_NOT_ALLOWED: { en: 'Method not allowed', zh: '请求方法不被允许' },\r\n RATE_LIMIT_EXCEEDED: { en: 'Rate limit exceeded', zh: '请求频率超限' },\r\n SYMBOL_NOT_FOUND: { en: 'Symbol not found', zh: '未找到符号' },\r\n BODY_TOO_LARGE: { en: 'Request body too large', zh: '请求体过大' },\r\n INVALID_JSON_BODY: { en: 'Invalid JSON body: expected object', zh: '无效的 JSON 请求体:应为对象' },\r\n INVALID_CONFIG: { en: 'Invalid configuration', zh: '无效的配置' },\r\n INDEX_NOT_FOUND: { en: 'Index not found', zh: '未找到索引' },\r\n DELETE_FAILED: { en: 'Failed to delete index', zh: '删除索引失败' },\r\n METRICS_FAILED: { en: 'Graph metrics computation failed', zh: '图指标计算失败' },\r\n SYNC_FAILED: { en: 'Upstream sync failed', zh: '上游同步失败' },\r\n};\r\nexport const REPO_URL = 'https://github.com/xuanyuanchumo/dsh-codegraph-visualizer';\r\nexport const IMPORT = {\r\n MAX_PASTE_BYTES: 10 * 1024 * 1024,\r\n} as const;\r\n\r\nexport const TOOLTIP = {\r\n OFFSET: 15,\r\n WIDTH: 200,\r\n HEIGHT: 80,\r\n} as const;\r\n\r\nexport const URL_STATE = {\r\n MAX_LENGTH: 2000,\r\n} as const;\r\n\r\nexport const DIFF_ENGINE = {\r\n MAX_DEEP_EQUAL_DEPTH: 50,\r\n} as const;\r\n\r\nexport const CORE_FUNCTION_ANALYZER = {\r\n MAX_CALL_CHAIN_RECURSION_DEPTH: 500,\r\n} as const;\r\n\r\nexport const CALL_CHAIN = {\r\n MAX_FUNCTIONS: 20,\r\n} as const;\r\n\r\nexport const FILE_SYSTEM = {\r\n MAX_FILES: 500,\r\n} as const;\r\n\r\nexport const CODEGRAPH_ADAPTER = {\r\n MAX_IMPORTS_PER_SOURCE: 20,\r\n} as const;\r\n\r\nexport const TREE_SITTER = {\r\n BATCH_SIZE: 20,\r\n} as const;\r\n\r\nexport const INDEXED_DB = {\r\n DB_VERSION: 2,\r\n TTL: {\r\n CLUSTER_MS: 30 * 60 * 1000,\r\n DIAGRAM_MS: 60 * 60 * 1000,\r\n OUTLINE_MS: 24 * 60 * 60 * 1000,\r\n AST_MS: 24 * 60 * 60 * 1000,\r\n EDGES_MS: 30 * 60 * 1000,\r\n },\r\n CAPACITY: {\r\n HIGH_WATERMARK: 0.85,\r\n LOW_WATERMARK: 0.70,\r\n },\r\n BATCH: {\r\n MAX_ENTRIES: 500,\r\n },\r\n} as const;\r\n\r\nexport const LOGGER = {\r\n RING_SIZE: 200,\r\n} as const;\r\n\r\nexport const LOUVAIN = {\r\n MAX_ITERATIONS: 30,\r\n} as const;\r\nexport const PERF = {\r\n CLUSTER_WARN_MS: 500,\r\n CLUSTER_DEGRADE_MS: 2000,\r\n LAYOUT_WARN_MS: 300,\r\n LAYOUT_DEGRADE_MS: 5000,\r\n RENDER_WARN_MS: 100,\r\n RENDER_DEGRADE_MS: 1000,\r\n DIAGRAM_WARN_MS: 200,\r\n DIAGRAM_DEGRADE_MS: 1000,\r\n AST_FETCH_WARN_MS: 1000,\r\n AST_FETCH_DEGRADE_MS: 5000,\r\n} as const;\r\n\r\n/**\r\n * 运行时层预算(cycle-88,P2-1 三层独立面板)。\r\n *\r\n * 验收原文 `docs/图谱按需加载.md:196-200` 的第三层一行:\r\n * 「运行时层 | Trace、日志、性能排查 | 独立面板,按时间窗口、请求 ID、Trace ID 加载\r\n * | 单次时间窗口 ≤ 5 分钟或 1000 条」,并在 `:265` 复述为\r\n * 「运行时层单次时间窗口 ≤ 5 分钟或 1000 条」。\r\n *\r\n * 两个上限是**同一层预算的两个维度**,取交集而不是并集:窗口先裁掉过老的数据,\r\n * 再按条数截断,于是「最近 5 分钟」与「最多 1000 条」同时成立。若只实现其中一个,\r\n * 高频日志场景仍能一次渲染上百万条——正是风险 9(`:243-246` 节点爆炸)想避免的。\r\n *\r\n * `MAX_ENTRIES` 刻意与 `LOGGER.RING_SIZE`(200)区分:环形缓冲是**保留**策略,\r\n * 这里是**单次展示**策略,两者不能互相替代(缓冲可能远大于一次要看的量)。\r\n */\r\nexport const RUNTIME = {\r\n /** 单次加载的时间窗口上限:5 分钟 */\r\n WINDOW_MS: 5 * 60 * 1000,\r\n /** 单次加载的条目数上限:1000 条 */\r\n MAX_ENTRIES: 1000,\r\n /** 面板里默认展示的日志条数(小于 MAX_ENTRIES,避免首屏即满) */\r\n PREVIEW_ENTRIES: 50,\r\n /** 单次展示的性能指标条数上限 */\r\n MAX_METRICS: 100,\r\n} as const;"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "./constants-CQq93l3e.js";
|
|
2
|
+
import { canonicalizeEdge, canonicalizeNode, computeContentHash, computeGraphEtag, computeGraphFingerprint, etagKeyForAst, etagKeyForChildren, etagKeyForEdges, etagKeyForOutline, hash64, resolveAstBudget, stableStringify } from "./graphFingerprint-PuxORZUF.js";
|
|
3
|
+
|
|
4
|
+
export { computeGraphFingerprint };
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { FINGERPRINT, SYMBOL_AST } from "./constants-CQq93l3e.js";
|
|
2
|
+
|
|
3
|
+
//#region src/shared/graphFingerprint.ts
|
|
4
|
+
/**
|
|
5
|
+
|
|
6
|
+
* Structural fingerprint + content hash for the code graph (P0-6).
|
|
7
|
+
|
|
8
|
+
*
|
|
9
|
+
|
|
10
|
+
* Why this exists: snapshot rows used to be keyed by
|
|
11
|
+
|
|
12
|
+
* `${nodeCount}:${edgeCount}:${metadata.timestamp}`, so every scan produced a brand-new hash.
|
|
13
|
+
|
|
14
|
+
* `idx_snapshots_repo_hash UNIQUE(repo_id, snapshot_hash)` could therefore never fire, and
|
|
15
|
+
|
|
16
|
+
* `pruneSnapshots()` was the only thing bounding the table.
|
|
17
|
+
|
|
18
|
+
*
|
|
19
|
+
|
|
20
|
+
* The fingerprint is derived purely from graph *content* (ids, structural fields and properties)
|
|
21
|
+
|
|
22
|
+
* and deliberately excludes `metadata.timestamp`, so two scans of an unchanged repository produce
|
|
23
|
+
|
|
24
|
+
* the same value and the snapshot write becomes idempotent.
|
|
25
|
+
|
|
26
|
+
*
|
|
27
|
+
|
|
28
|
+
* The digest is implemented in plain arithmetic rather than `node:crypto`: this module lives under
|
|
29
|
+
|
|
30
|
+
* `src/shared/**`, which `tsconfig.client.json` type-checks and `scripts/build-client.mjs` pulls
|
|
31
|
+
|
|
32
|
+
* into the browser bundle. It is a change-detection hash, not a cryptographic digest.
|
|
33
|
+
|
|
34
|
+
*/
|
|
35
|
+
/** Separates fields inside one canonical record; never appears in adapter-produced values. */
|
|
36
|
+
const FIELD_SEP = "";
|
|
37
|
+
/** Separates records in the hashed payload. */
|
|
38
|
+
const RECORD_SEP = "";
|
|
39
|
+
/** Standard FNV-1a 32-bit offset basis and prime. */
|
|
40
|
+
const FNV_OFFSET_A = 2166136261;
|
|
41
|
+
const FNV_PRIME = 16777619;
|
|
42
|
+
/** A second, independent lane keeps the combined digest 64-bit wide. */
|
|
43
|
+
const FNV_OFFSET_B = 2654435769;
|
|
44
|
+
function toHex32(value) {
|
|
45
|
+
return (value >>> 0).toString(16).padStart(8, "0");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
|
|
49
|
+
* Two independent FNV-1a 32-bit lanes combined into a 64-bit (16 hex digit) digest. `Math.imul`
|
|
50
|
+
|
|
51
|
+
* gives the exact 32-bit multiplication FNV requires without BigInt, so hashing a multi-megabyte
|
|
52
|
+
|
|
53
|
+
* payload stays cheap.
|
|
54
|
+
|
|
55
|
+
*/
|
|
56
|
+
function hash64(input) {
|
|
57
|
+
let a = FNV_OFFSET_A;
|
|
58
|
+
let b = FNV_OFFSET_B;
|
|
59
|
+
for (let i = 0; i < input.length; i++) {
|
|
60
|
+
const code = input.charCodeAt(i);
|
|
61
|
+
a = Math.imul(a ^ code & 255, FNV_PRIME);
|
|
62
|
+
b = Math.imul(b ^ code & 255, FNV_PRIME);
|
|
63
|
+
const high = code >>> 8;
|
|
64
|
+
if (high !== 0) {
|
|
65
|
+
a = Math.imul(a ^ high, FNV_PRIME);
|
|
66
|
+
b = Math.imul(b ^ high, FNV_PRIME);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return toHex32(a) + toHex32(b);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
|
|
73
|
+
* Deterministic JSON-ish serialisation: object keys are sorted recursively and `undefined`
|
|
74
|
+
|
|
75
|
+
* object values are dropped, so two structurally equal values always render identically.
|
|
76
|
+
|
|
77
|
+
*/
|
|
78
|
+
function stableStringify(value) {
|
|
79
|
+
if (value === null) return "null";
|
|
80
|
+
if (value === void 0) return "undefined";
|
|
81
|
+
const t = typeof value;
|
|
82
|
+
if (t === "string") return JSON.stringify(value);
|
|
83
|
+
if (t === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
84
|
+
if (t === "boolean") return value ? "true" : "false";
|
|
85
|
+
if (t === "bigint") return `${String(value)}n`;
|
|
86
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
87
|
+
if (t === "object") {
|
|
88
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([x], [y]) => x < y ? -1 : x > y ? 1 : 0);
|
|
89
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
90
|
+
}
|
|
91
|
+
return "null";
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
|
|
95
|
+
* Canonical record for one node. Ordering, whitespace and `metadata` never leak in; only the
|
|
96
|
+
|
|
97
|
+
* fields that describe the node's identity and structure do.
|
|
98
|
+
|
|
99
|
+
*/
|
|
100
|
+
function canonicalizeNode(node) {
|
|
101
|
+
return [
|
|
102
|
+
"n",
|
|
103
|
+
node.id,
|
|
104
|
+
node.label,
|
|
105
|
+
node.type,
|
|
106
|
+
node.filePath,
|
|
107
|
+
String(node.lineNumber ?? 0),
|
|
108
|
+
node.parentId ?? "",
|
|
109
|
+
node.layer ?? "",
|
|
110
|
+
stableStringify(node.properties ?? {})
|
|
111
|
+
].join(FIELD_SEP);
|
|
112
|
+
}
|
|
113
|
+
/** Canonical record for one edge. `id` participates, so re-keyed edges count as a change. */
|
|
114
|
+
function canonicalizeEdge(edge) {
|
|
115
|
+
return [
|
|
116
|
+
"e",
|
|
117
|
+
edge.id,
|
|
118
|
+
edge.source,
|
|
119
|
+
edge.target,
|
|
120
|
+
edge.type,
|
|
121
|
+
edge.direction ?? "",
|
|
122
|
+
edge.weight === void 0 ? "" : String(edge.weight),
|
|
123
|
+
stableStringify(edge.properties ?? {})
|
|
124
|
+
].join(FIELD_SEP);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
|
|
128
|
+
* Hash of a raw text payload (a file's source, a serialized graph). Used by the tree-sitter route
|
|
129
|
+
|
|
130
|
+
* so identical input is recognisable without re-parsing.
|
|
131
|
+
|
|
132
|
+
*/
|
|
133
|
+
function computeContentHash(content) {
|
|
134
|
+
return `v${FINGERPRINT.VERSION}:${hash64(content)}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
|
|
138
|
+
* 请求键:同一资源下**区分不同请求参数**的维度(P3-R18-02)。
|
|
139
|
+
|
|
140
|
+
*
|
|
141
|
+
|
|
142
|
+
* 为什么资源维度还不够:`children` / `outline` / `ast` / `edges` 四条路由是**参数化**的,
|
|
143
|
+
|
|
144
|
+
* 而缓存图只有一个槽。只按 (工作区, 版本, 资源) 定域时,`children(nodeA)` 与 `children(nodeB)`
|
|
145
|
+
|
|
146
|
+
* 会算出**同一个校验值** —— 客户端拿着 A 的校验值去问 B,host 回 304,B 的子节点永远加载不出来。
|
|
147
|
+
|
|
148
|
+
* 这与 P0-R16-02「校验器与被校验资源无关」是同一类缺陷,只是深了一层:校验器还必须与被校验的
|
|
149
|
+
|
|
150
|
+
* **那一次请求**有关。
|
|
151
|
+
|
|
152
|
+
*
|
|
153
|
+
|
|
154
|
+
* 每条键必须覆盖**所有会改变响应载荷的参数**,一个都不能少。少一个参数就等价于"两个不同响应
|
|
155
|
+
|
|
156
|
+
* 共用一个校验值",假 304 会把其中一个响应的内容当成另一个的答案。因此:
|
|
157
|
+
|
|
158
|
+
* - `children` 含 `limit`(不同页大小返回不同页);
|
|
159
|
+
|
|
160
|
+
* - `ast` 含 `budget`(预算直接决定切多少节点);
|
|
161
|
+
|
|
162
|
+
* - `edges` 含 `limit` 与 `detail`(`detail` 决定聚合与否,是响应形状的分水岭)。
|
|
163
|
+
|
|
164
|
+
*
|
|
165
|
+
|
|
166
|
+
* 四个构造函数是本文件的唯一真源:host 写 200 与客户端存/回传都必须调用同一对函数,
|
|
167
|
+
|
|
168
|
+
* 否则两侧键格式一旦漂移,304 要么永不命中(每次都全量重取),要么**假命中**(静默丢一次真实加载)。
|
|
169
|
+
|
|
170
|
+
*/
|
|
171
|
+
function etagKeyForChildren(nodeId, cursor, limit) {
|
|
172
|
+
return `children:${nodeId}:${cursor ?? ""}:${limit}`;
|
|
173
|
+
}
|
|
174
|
+
function etagKeyForOutline(nodeId) {
|
|
175
|
+
return `outline:${nodeId}`;
|
|
176
|
+
}
|
|
177
|
+
function etagKeyForAst(nodeId, cursor, budget) {
|
|
178
|
+
return `ast:${nodeId}:${cursor}:${budget}`;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
|
|
182
|
+
* `budget` 的规范化解析。host 与客户端**必须**共用本函数。
|
|
183
|
+
|
|
184
|
+
*
|
|
185
|
+
|
|
186
|
+
* 客户端从来不发送 `budget`(它只用 `cursor` 分段),因此请求键里的 budget 在两侧都等于
|
|
187
|
+
|
|
188
|
+
* `SYMBOL_AST.MAX_NODES`——但"等于"必须是**算出来的**,不能是两边各写一个常量字面量:
|
|
189
|
+
|
|
190
|
+
* 一旦 host 改成发送/钳制预算,两侧键就会静默分叉,校验值永远匹配不上(每次全量重取),
|
|
191
|
+
|
|
192
|
+
* 而这种失败没有任何测试会红——它只表现为"缓存没生效"。
|
|
193
|
+
|
|
194
|
+
*/
|
|
195
|
+
function resolveAstBudget(raw) {
|
|
196
|
+
const parsed = parseInt(raw ?? String(SYMBOL_AST.MAX_NODES), 10);
|
|
197
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return SYMBOL_AST.MAX_NODES;
|
|
198
|
+
return Math.min(parsed, SYMBOL_AST.MAX_NODES);
|
|
199
|
+
}
|
|
200
|
+
function etagKeyForEdges(fromId, scope, cursor, limit, detail) {
|
|
201
|
+
return `edges:${fromId}:${scope}:${cursor}:${limit}:${detail ? 1 : 0}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
|
|
205
|
+
* ETag for a graph resource, **scoped to both the workspace identity and the resource dimension**.
|
|
206
|
+
|
|
207
|
+
*
|
|
208
|
+
|
|
209
|
+
* 为什么不能只回 `String(metadata.timestamp)`:host 只保留一个 `lastGraphData` 槽,槽里可能装着另一个
|
|
210
|
+
|
|
211
|
+
* 工作区的图。若校验值只有时间戳,两个工作区在毫秒级上撞到同一个 timestamp 时,客户端带着 A 工作区
|
|
212
|
+
|
|
213
|
+
* 的校验值去问 B 工作区,host 会回 304,客户端于是保留旧画布并**静默丢弃一次真实更新**。把 `repoId`
|
|
214
|
+
|
|
215
|
+
* 混进校验值后,不同工作区的 ETag 必然不同,"校验器与被校验资源无关"这类缺陷在结构上不再可能。
|
|
216
|
+
|
|
217
|
+
*
|
|
218
|
+
|
|
219
|
+
* 为什么还要 `resource` 维度:`/data`、`/bootstrap`、`/children`、`/outline`、`/ast`、`/edges` 全都拿
|
|
220
|
+
|
|
221
|
+
* 同一个单槽缓存图去比对。若校验值不含资源维度,客户端持有的 `/data` 校验值就能让 `/children` 回
|
|
222
|
+
|
|
223
|
+
* 304——"校验器与被校验资源无关"的同一条缺陷换了个形状。该参数**必填**(不给默认值):任何新增调用点
|
|
224
|
+
|
|
225
|
+
* 都被类型检查强制回答"我校验的是哪一份资源",与 `fetchGraphDataIncremental` 的 `scopeRepoId` 同一设计。
|
|
226
|
+
|
|
227
|
+
*
|
|
228
|
+
|
|
229
|
+
* 值仍然是"弱校验器"语义(`W/` 前缀):它随内容变化,但不是逐字节摘要。刻意**不**改用
|
|
230
|
+
|
|
231
|
+
* `computeGraphFingerprint()`——指纹要遍历全部节点与边,而 ETag 在每次 `/data` 请求上都会被计算;
|
|
232
|
+
|
|
233
|
+
* 这里的目的是**身份定域**,不是内容寻址。
|
|
234
|
+
|
|
235
|
+
*
|
|
236
|
+
|
|
237
|
+
* `requestKey` 是第三个身份维度(P3-R18-02)。`children` / `outline` / `ast` / `edges` 四条路由
|
|
238
|
+
|
|
239
|
+
* 是**参数化**的,而缓存图只有一个槽:只按 (工作区, 版本, 资源) 定域时,`children(nodeA)` 与
|
|
240
|
+
|
|
241
|
+
* `children(nodeB)` 会算出同一个校验值,客户端拿 A 的校验值问 B 就会收到 304,B 的子节点永远
|
|
242
|
+
|
|
243
|
+
* 加载不出来。请求键由 `etagKeyFor*` 构造函数产出,host 与客户端必须共用同一对函数。
|
|
244
|
+
|
|
245
|
+
*/
|
|
246
|
+
function computeGraphEtag(repoId, revision, resource, requestKey) {
|
|
247
|
+
const identity = requestKey === void 0 ? `${resource}\u0001${repoId ?? ""}` : `${resource}\u0001${repoId ?? ""}\u0001${requestKey}`;
|
|
248
|
+
return `W/"${revision}-${hash64(identity)}"`;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
|
|
252
|
+
* Fingerprint of a graph's content. Independent of node/edge ordering and of `metadata.timestamp`,
|
|
253
|
+
|
|
254
|
+
* so it is stable across repeated scans of an unchanged repository.
|
|
255
|
+
|
|
256
|
+
*/
|
|
257
|
+
function computeGraphFingerprint(data) {
|
|
258
|
+
const nodeLines = data.nodes.map(canonicalizeNode).sort();
|
|
259
|
+
const edgeLines = data.edges.map(canonicalizeEdge).sort();
|
|
260
|
+
const meta = data.metadata;
|
|
261
|
+
const header = [
|
|
262
|
+
`nodes=${nodeLines.length}`,
|
|
263
|
+
`edges=${edgeLines.length}`,
|
|
264
|
+
`totalNodes=${meta.totalNodeCount ?? nodeLines.length}`,
|
|
265
|
+
`totalEdges=${meta.totalEdgeCount ?? edgeLines.length}`,
|
|
266
|
+
`truncated=${meta.truncated === true ? 1 : 0}`
|
|
267
|
+
].join(FIELD_SEP);
|
|
268
|
+
return `v${FINGERPRINT.VERSION}:${hash64([
|
|
269
|
+
header,
|
|
270
|
+
...nodeLines,
|
|
271
|
+
...edgeLines
|
|
272
|
+
].join(RECORD_SEP))}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
export { canonicalizeEdge, canonicalizeNode, computeContentHash, computeGraphEtag, computeGraphFingerprint, etagKeyForAst, etagKeyForChildren, etagKeyForEdges, etagKeyForOutline, hash64, resolveAstBudget, stableStringify };
|
|
277
|
+
//# sourceMappingURL=graphFingerprint-PuxORZUF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graphFingerprint-PuxORZUF.js","names":["value: number","input: string","value: unknown","node: GraphNode","edge: GraphEdge","content: string","nodeId: string","cursor: string | null | undefined","limit: number","cursor: number","budget: number","raw: string | null | undefined","fromId: string","scope: string","detail: boolean","repoId: string | null | undefined","revision: number","resource: GraphEtagResource","requestKey?: string","data: GraphData"],"sources":["../src/shared/graphFingerprint.ts"],"sourcesContent":["import type { GraphData, GraphEdge, GraphNode } from '../types/index.ts';\r\nimport { FINGERPRINT, SYMBOL_AST } from './constants.ts';\r\n\r\n/**\r\n * Structural fingerprint + content hash for the code graph (P0-6).\r\n *\r\n * Why this exists: snapshot rows used to be keyed by\r\n * `${nodeCount}:${edgeCount}:${metadata.timestamp}`, so every scan produced a brand-new hash.\r\n * `idx_snapshots_repo_hash UNIQUE(repo_id, snapshot_hash)` could therefore never fire, and\r\n * `pruneSnapshots()` was the only thing bounding the table.\r\n *\r\n * The fingerprint is derived purely from graph *content* (ids, structural fields and properties)\r\n * and deliberately excludes `metadata.timestamp`, so two scans of an unchanged repository produce\r\n * the same value and the snapshot write becomes idempotent.\r\n *\r\n * The digest is implemented in plain arithmetic rather than `node:crypto`: this module lives under\r\n * `src/shared/**`, which `tsconfig.client.json` type-checks and `scripts/build-client.mjs` pulls\r\n * into the browser bundle. It is a change-detection hash, not a cryptographic digest.\r\n */\r\n\r\n/** Separates fields inside one canonical record; never appears in adapter-produced values. */\r\nconst FIELD_SEP = '\\u0001';\r\n/** Separates records in the hashed payload. */\r\nconst RECORD_SEP = '\\u0002';\r\n\r\n/** Standard FNV-1a 32-bit offset basis and prime. */\r\nconst FNV_OFFSET_A = 0x811c9dc5;\r\nconst FNV_PRIME = 0x01000193;\r\n/** A second, independent lane keeps the combined digest 64-bit wide. */\r\nconst FNV_OFFSET_B = 0x9e3779b9;\r\n\r\nfunction toHex32(value: number): string {\r\n return (value >>> 0).toString(16).padStart(8, '0');\r\n}\r\n\r\n/**\r\n * Two independent FNV-1a 32-bit lanes combined into a 64-bit (16 hex digit) digest. `Math.imul`\r\n * gives the exact 32-bit multiplication FNV requires without BigInt, so hashing a multi-megabyte\r\n * payload stays cheap.\r\n */\r\nexport function hash64(input: string): string {\r\n let a = FNV_OFFSET_A;\r\n let b = FNV_OFFSET_B;\r\n for (let i = 0; i < input.length; i++) {\r\n const code = input.charCodeAt(i);\r\n a = Math.imul(a ^ (code & 0xff), FNV_PRIME);\r\n b = Math.imul(b ^ (code & 0xff), FNV_PRIME);\r\n const high = code >>> 8;\r\n if (high !== 0) {\r\n a = Math.imul(a ^ high, FNV_PRIME);\r\n b = Math.imul(b ^ high, FNV_PRIME);\r\n }\r\n }\r\n return toHex32(a) + toHex32(b);\r\n}\r\n\r\n/**\r\n * Deterministic JSON-ish serialisation: object keys are sorted recursively and `undefined`\r\n * object values are dropped, so two structurally equal values always render identically.\r\n */\r\nexport function stableStringify(value: unknown): string {\r\n if (value === null) return 'null';\r\n if (value === undefined) return 'undefined';\r\n const t = typeof value;\r\n if (t === 'string') return JSON.stringify(value);\r\n if (t === 'number') return Number.isFinite(value) ? String(value) : 'null';\r\n if (t === 'boolean') return value ? 'true' : 'false';\r\n if (t === 'bigint') return `${String(value)}n`;\r\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;\r\n if (t === 'object') {\r\n const entries = Object.entries(value as Record<string, unknown>)\r\n .filter(([, v]) => v !== undefined)\r\n .sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0));\r\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(',')}}`;\r\n }\r\n // Functions and symbols never appear in adapter-produced graph payloads.\r\n return 'null';\r\n}\r\n\r\n/**\r\n * Canonical record for one node. Ordering, whitespace and `metadata` never leak in; only the\r\n * fields that describe the node's identity and structure do.\r\n */\r\nexport function canonicalizeNode(node: GraphNode): string {\r\n return [\r\n 'n',\r\n node.id,\r\n node.label,\r\n node.type,\r\n node.filePath,\r\n String(node.lineNumber ?? 0),\r\n node.parentId ?? '',\r\n node.layer ?? '',\r\n stableStringify(node.properties ?? {}),\r\n ].join(FIELD_SEP);\r\n}\r\n\r\n/** Canonical record for one edge. `id` participates, so re-keyed edges count as a change. */\r\nexport function canonicalizeEdge(edge: GraphEdge): string {\r\n return [\r\n 'e',\r\n edge.id,\r\n edge.source,\r\n edge.target,\r\n edge.type,\r\n edge.direction ?? '',\r\n edge.weight === undefined ? '' : String(edge.weight),\r\n stableStringify(edge.properties ?? {}),\r\n ].join(FIELD_SEP);\r\n}\r\n\r\n/**\r\n * Hash of a raw text payload (a file's source, a serialized graph). Used by the tree-sitter route\r\n * so identical input is recognisable without re-parsing.\r\n */\r\nexport function computeContentHash(content: string): string {\r\n return `v${FINGERPRINT.VERSION}:${hash64(content)}`;\r\n}\r\n\r\n/** 会被 ETag 定域的数据端点。`/data` 之外的路由返回的都是派生数据,与缓存图不是同一份资源。 */\r\nexport type GraphEtagResource = 'graph' | 'bootstrap' | 'children' | 'outline' | 'ast' | 'edges';\r\n\r\n/**\r\n * 请求键:同一资源下**区分不同请求参数**的维度(P3-R18-02)。\r\n *\r\n * 为什么资源维度还不够:`children` / `outline` / `ast` / `edges` 四条路由是**参数化**的,\r\n * 而缓存图只有一个槽。只按 (工作区, 版本, 资源) 定域时,`children(nodeA)` 与 `children(nodeB)`\r\n * 会算出**同一个校验值** —— 客户端拿着 A 的校验值去问 B,host 回 304,B 的子节点永远加载不出来。\r\n * 这与 P0-R16-02「校验器与被校验资源无关」是同一类缺陷,只是深了一层:校验器还必须与被校验的\r\n * **那一次请求**有关。\r\n *\r\n * 每条键必须覆盖**所有会改变响应载荷的参数**,一个都不能少。少一个参数就等价于\"两个不同响应\r\n * 共用一个校验值\",假 304 会把其中一个响应的内容当成另一个的答案。因此:\r\n * - `children` 含 `limit`(不同页大小返回不同页);\r\n * - `ast` 含 `budget`(预算直接决定切多少节点);\r\n * - `edges` 含 `limit` 与 `detail`(`detail` 决定聚合与否,是响应形状的分水岭)。\r\n *\r\n * 四个构造函数是本文件的唯一真源:host 写 200 与客户端存/回传都必须调用同一对函数,\r\n * 否则两侧键格式一旦漂移,304 要么永不命中(每次都全量重取),要么**假命中**(静默丢一次真实加载)。\r\n */\r\nexport function etagKeyForChildren(nodeId: string, cursor: string | null | undefined, limit: number): string {\r\n return `children:${nodeId}:${cursor ?? ''}:${limit}`;\r\n}\r\n\r\nexport function etagKeyForOutline(nodeId: string): string {\r\n return `outline:${nodeId}`;\r\n}\r\n\r\nexport function etagKeyForAst(nodeId: string, cursor: number, budget: number): string {\r\n return `ast:${nodeId}:${cursor}:${budget}`;\r\n}\r\n\r\n/**\r\n * `budget` 的规范化解析。host 与客户端**必须**共用本函数。\r\n *\r\n * 客户端从来不发送 `budget`(它只用 `cursor` 分段),因此请求键里的 budget 在两侧都等于\r\n * `SYMBOL_AST.MAX_NODES`——但\"等于\"必须是**算出来的**,不能是两边各写一个常量字面量:\r\n * 一旦 host 改成发送/钳制预算,两侧键就会静默分叉,校验值永远匹配不上(每次全量重取),\r\n * 而这种失败没有任何测试会红——它只表现为\"缓存没生效\"。\r\n */\r\nexport function resolveAstBudget(raw: string | null | undefined): number {\r\n const parsed = parseInt(raw ?? String(SYMBOL_AST.MAX_NODES), 10);\r\n // `parsed || MAX` 只挡住 NaN 与 0:`?budget=-5` 会原样穿过,随后 `slice(cursor, cursor + budget)`\r\n // 得到空页,且请求键里出现 `ast:sym:0:-5` —— 一个所有负数请求共用的键(不同预算共用校验值)。\r\n // 负预算没有任何合法语义(预算就是\"最多给多少\"),故与畸形输入一并回落。\r\n if (!Number.isFinite(parsed) || parsed <= 0) return SYMBOL_AST.MAX_NODES;\r\n return Math.min(parsed, SYMBOL_AST.MAX_NODES);\r\n}\r\n\r\nexport function etagKeyForEdges(\r\n fromId: string,\r\n scope: string,\r\n cursor: number,\r\n limit: number,\r\n detail: boolean,\r\n): string {\r\n return `edges:${fromId}:${scope}:${cursor}:${limit}:${detail ? 1 : 0}`;\r\n}\r\n\r\n/**\r\n * ETag for a graph resource, **scoped to both the workspace identity and the resource dimension**.\r\n *\r\n * 为什么不能只回 `String(metadata.timestamp)`:host 只保留一个 `lastGraphData` 槽,槽里可能装着另一个\r\n * 工作区的图。若校验值只有时间戳,两个工作区在毫秒级上撞到同一个 timestamp 时,客户端带着 A 工作区\r\n * 的校验值去问 B 工作区,host 会回 304,客户端于是保留旧画布并**静默丢弃一次真实更新**。把 `repoId`\r\n * 混进校验值后,不同工作区的 ETag 必然不同,\"校验器与被校验资源无关\"这类缺陷在结构上不再可能。\r\n *\r\n * 为什么还要 `resource` 维度:`/data`、`/bootstrap`、`/children`、`/outline`、`/ast`、`/edges` 全都拿\r\n * 同一个单槽缓存图去比对。若校验值不含资源维度,客户端持有的 `/data` 校验值就能让 `/children` 回\r\n * 304——\"校验器与被校验资源无关\"的同一条缺陷换了个形状。该参数**必填**(不给默认值):任何新增调用点\r\n * 都被类型检查强制回答\"我校验的是哪一份资源\",与 `fetchGraphDataIncremental` 的 `scopeRepoId` 同一设计。\r\n *\r\n * 值仍然是\"弱校验器\"语义(`W/` 前缀):它随内容变化,但不是逐字节摘要。刻意**不**改用\r\n * `computeGraphFingerprint()`——指纹要遍历全部节点与边,而 ETag 在每次 `/data` 请求上都会被计算;\r\n * 这里的目的是**身份定域**,不是内容寻址。\r\n *\r\n * `requestKey` 是第三个身份维度(P3-R18-02)。`children` / `outline` / `ast` / `edges` 四条路由\r\n * 是**参数化**的,而缓存图只有一个槽:只按 (工作区, 版本, 资源) 定域时,`children(nodeA)` 与\r\n * `children(nodeB)` 会算出同一个校验值,客户端拿 A 的校验值问 B 就会收到 304,B 的子节点永远\r\n * 加载不出来。请求键由 `etagKeyFor*` 构造函数产出,host 与客户端必须共用同一对函数。\r\n */\r\nexport function computeGraphEtag(\r\n repoId: string | null | undefined,\r\n revision: number,\r\n resource: GraphEtagResource,\r\n requestKey?: string,\r\n): string {\r\n // 省略 `requestKey` 时保持原字符串形状不变:`/data` 与 `/bootstrap` 的校验值对已部署的客户端\r\n // 必须逐字节稳定,否则一次升级就会让所有在途校验值失配(多回一次 200 是安全的,但没必要)。\r\n const identity = requestKey === undefined\r\n ? `${resource}\\u0001${repoId ?? ''}`\r\n : `${resource}\\u0001${repoId ?? ''}\\u0001${requestKey}`;\r\n return `W/\"${revision}-${hash64(identity)}\"`;\r\n}\r\n\r\n/**\r\n * Fingerprint of a graph's content. Independent of node/edge ordering and of `metadata.timestamp`,\r\n * so it is stable across repeated scans of an unchanged repository.\r\n */\r\nexport function computeGraphFingerprint(data: GraphData): string {\r\n const nodeLines = data.nodes.map(canonicalizeNode).sort();\r\n const edgeLines = data.edges.map(canonicalizeEdge).sort();\r\n const meta = data.metadata;\r\n const header = [\r\n `nodes=${nodeLines.length}`,\r\n `edges=${edgeLines.length}`,\r\n `totalNodes=${meta.totalNodeCount ?? nodeLines.length}`,\r\n `totalEdges=${meta.totalEdgeCount ?? edgeLines.length}`,\r\n `truncated=${meta.truncated === true ? 1 : 0}`,\r\n ].join(FIELD_SEP);\r\n return `v${FINGERPRINT.VERSION}:${hash64([header, ...nodeLines, ...edgeLines].join(RECORD_SEP))}`;\r\n}\r\n"],"mappings":"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"}
|