homegraph 1.6.0 → 1.6.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +64 -14
  3. package/dist/bin/homegraph.js +2 -2
  4. package/dist/db/index.js +1 -0
  5. package/dist/db/migrations.d.ts +6 -1
  6. package/dist/db/migrations.js +26 -1
  7. package/dist/db/queries.d.ts +9 -0
  8. package/dist/db/queries.js +32 -7
  9. package/dist/db/schema.sql +4 -0
  10. package/dist/extraction/grammars.js +2 -1
  11. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  12. package/dist/extraction/languages/c-cpp.js +44 -0
  13. package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
  14. package/dist/extraction/wasm-runtime-flags.js +9 -0
  15. package/dist/graph/evidence-paths.d.ts +2 -0
  16. package/dist/graph/evidence-paths.js +20 -3
  17. package/dist/index.js +7 -1
  18. package/dist/installer/targets/icode.d.ts +43 -0
  19. package/dist/installer/targets/icode.js +631 -0
  20. package/dist/installer/targets/registry.js +2 -0
  21. package/dist/installer/targets/types.d.ts +1 -1
  22. package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
  23. package/dist/mcp/arkts-evidence-packs.js +218 -10
  24. package/dist/mcp/evidence-audit.d.ts +3 -0
  25. package/dist/mcp/evidence-audit.js +40 -0
  26. package/dist/mcp/evidence-rendering.js +1 -1
  27. package/dist/mcp/explore-repeat-guard.d.ts +19 -5
  28. package/dist/mcp/explore-repeat-guard.js +35 -14
  29. package/dist/mcp/explore-session-state.d.ts +5 -0
  30. package/dist/mcp/explore-session-state.js +1 -0
  31. package/dist/mcp/implementation-context.d.ts +35 -0
  32. package/dist/mcp/implementation-context.js +232 -0
  33. package/dist/mcp/locate-contract.js +1 -1
  34. package/dist/mcp/query-cache.d.ts +1 -15
  35. package/dist/mcp/query-cache.js +20 -11
  36. package/dist/mcp/request-evidence.d.ts +69 -0
  37. package/dist/mcp/request-evidence.js +297 -0
  38. package/dist/mcp/server-instructions.d.ts +2 -2
  39. package/dist/mcp/server-instructions.js +15 -15
  40. package/dist/mcp/session.js +3 -2
  41. package/dist/mcp/tools.d.ts +39 -12
  42. package/dist/mcp/tools.js +237 -121
  43. package/dist/resolution/c-feature-variants.d.ts +40 -0
  44. package/dist/resolution/c-feature-variants.js +272 -0
  45. package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
  46. package/dist/resolution/cpp-macro-visibility.js +393 -0
  47. package/dist/resolution/index.js +11 -0
  48. package/dist/resolution/name-matcher.js +49 -2
  49. package/dist/resolution/types.d.ts +8 -0
  50. package/dist/resolution/types.js +8 -0
  51. package/dist/search/literal-evidence.js +11 -3
  52. package/dist/search/query-plan-provider.js +13 -5
  53. package/dist/search/query-plan.d.ts +3 -1
  54. package/dist/search/query-plan.js +11 -4
  55. package/dist/search/request-contract.d.ts +29 -0
  56. package/dist/search/request-contract.js +76 -0
  57. package/dist/sync/watcher.d.ts +30 -0
  58. package/dist/sync/watcher.js +45 -0
  59. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,34 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10
10
  ## [Unreleased]
11
11
 
12
12
 
13
+ ## [1.6.2] - 2026-10-19
14
+
15
+ ### New Features
16
+
17
+ - **iCode support** in the installer. `homegraph install --target icode` wires the MCP server into iCode agent profiles (`~/.chrys/agents/` on macOS/Linux, `%APPDATA%\chrys\agents\` on Windows). The target is global-only. It edits existing profiles in place and, when the iCode runtime is found, writes shadow profiles for the built-in agents. If the runtime is missing, install still updates existing profiles and prints manual guidance. `HOMEGRAPH_ICODE_NO_BUILTINS=1` skips shadows. No instructions file is written (Spec 0057).
18
+
19
+ ### Improvements
20
+
21
+ - `homegraph_explore` no longer stops after 2 calls on a long-lived MCP connection. Another explore within 10 seconds of the previous result is refused; a later call, including a different question, is allowed. The tool description, initialize instructions, and explore footer state that wait (Spec 0058).
22
+ - MCP initialize / `homegraph_explore` description: drop bash-first and "HomeGraph is optional" wording that conflicted with DevEco Code's graph-first product prompt; existing-code tasks open with `homegraph_explore` alone. Tool list, budgets, and evidence caveats unchanged.
23
+ - MCP under Bun skips the Node/V8 `--liftoff-only` relaunch (those flags do not apply), so product hosts can spawn `homegraph.js` with the host Bun binary (Spec 0052).
24
+ - MCP product hosts that prefix tools with the server name (e.g. DevEco Code) can set `HOMEGRAPH_MCP_SHORT_TOOL_NAMES=1` so tools/list exposes short names (`explore`, `project`) and tools/call still accepts both short and `homegraph_*` forms — avoiding a doubled `homegraph_homegraph_*` name after the host prefix. Unset by default; Cursor/Claude keep canonical `homegraph_*` names.
25
+
26
+ ### Fixes
27
+
28
+ - C/C++: a call no longer binds another file's `static` function when a definition with external linkage exists. A C definition compiled only under a feature flag nothing in the project sets yields to the definition the default build compiles (Spec 0056).
29
+ - Windows: a file the daemon only read no longer shows up as edited. Watcher events whose size and modification time still match the index are ignored, so an NTFS last-access update does not mark the file pending sync (Spec 0055).
30
+ - C/C++: a header whose only C++ signal is `struct Derived : Base` is indexed as C++. A call that names a function-like macro visible at that line is not linked to a same-named function, and the include walk that decides this is bounded so a deep guard graph cannot blow the heap (Spec 0054).
31
+ - MCP `homegraph_explore` call budget shown to agents is **2** on every repo size, matching the session hard cap. Output sizing tiers are unchanged. Heuristic edges store `synthesized_by`, and ArkTS cross-file call cleanup deletes that channel by column instead of a JSON text match. Explore and Partial replies no longer tell the agent to call `homegraph_node`. Name search still tries FTS first, including Chinese literals (Spec 0053).
32
+ - MCP tools/list: when a session has no default project, `projectPath` is already marked `required` in the schema — its property description no longer says "Omit to use this session's default project", so agents are not told the field is both required and optional.
33
+
34
+ ## [1.6.1] - 2026-09-28
35
+
36
+ ### Improvements
37
+
38
+ - MCP default tools/list is now the product slim pair — `homegraph_explore`, `homegraph_project` — so tool schemas stop dominating host context. Full catalog via `HOMEGRAPH_MCP_TOOLS=all` (or `*`) or an explicit comma list (e.g. add `arkui_migrate` when needed). MCP initialize instructions and the explore tool description match the slim surface (no pointers to tools hidden by default) (Spec 0051).
39
+ - ArkTS explore can bundle verified render/navigation scope, used imports and types, explicit module exports and local dependency configuration with the located source. Attached SDK modules provide bounded method/parameter signatures, while control summaries distinguish icon appearance, click guards and direct enabled bindings. Whole-group budgets and source fingerprints preserve evidence boundaries; optional local pack audit supports error analysis. No additional model call or mandatory tool (Spec 0050).
40
+
13
41
  ## [1.6.0] - 2026-09-23
14
42
 
15
43
  ### Improvements
@@ -38,6 +66,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
38
66
 
39
67
  ### Improvements
40
68
 
69
+ - ArkTS explore can retain grounded page/literal/object targets and report request evidence separately from source completeness. Verified joint targets improve candidate ordering; a scoped enabled-binding check distinguishes actual modifiers from appearance-only controls while keeping runtime behavior unverified. Results honor source fingerprints and pack budgets, with independent target/behavior switches. Existing precise relationship and resource routes remain available (Spec 0049).
41
70
  - Bump `arkanalyzer` to **1.0.94**.
42
71
  - Harmony projects with a root `build-profile.json5` now default to **serial indexing** after ArkAnalyzer: one parse worker and no resolve/synthesis worker pool. On SceneBoard-scale repos this cut peak process-tree PrivateMemory from ~8.5GB to ~3.8GB with only ~6% more wall clock. Opt out with `HOMEGRAPH_HARMONY_SERIAL=0`; force on with `=1`. Explicit `CODEGRAPH_PARSE_WORKERS` still overrides parse concurrency (Spec 0037).
43
72
  - MCP product index readiness is now five short states (`empty` / `fast` / `full` / `dirty` / `syncing`): tool replies use a one-line `HomeGraph status=…` footer (or that line alone when the tool cannot answer yet). Pending edits surface as `dirty` with path list instead of the long stale banner; write locks stay `syncing`. MCP initialize includes a one-line status glossary (Spec 0035).
@@ -274,3 +303,5 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
274
303
  [1.5.8]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.5.8
275
304
  [1.5.10]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.5.10
276
305
  [1.6.0]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.0
306
+ [1.6.1]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.1
307
+ [1.6.2]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.2
package/README.md CHANGED
@@ -171,29 +171,76 @@ homegraph serve mcp # 启动 MCP 服务(一般由 Agent 自动拉
171
171
 
172
172
  Agent 侧工具名前缀为 `homegraph_`。
173
173
 
174
- **暴露规则:** 默认注册全部工具。索引文件数 **少于 500** 的小项目会自动收缩为三个核心工具(`explore` / `search` / `node`)。可通过环境变量 `HOMEGRAPH_MCP_TOOLS`(逗号分隔短名,如 `explore,node`)自定义暴露列表。
174
+ ### 默认暴露(下载即用)
175
175
 
176
- **跨项目查询:** 所有工具均支持可选参数 `projectPath`(绝对路径),用于在 monorepo 中查询子项目,或当 MCP 服务器根目录没有索引时指定目标项目。
176
+ 未设置 `HOMEGRAPH_MCP_TOOLS` 时,`tools/list` **只注册下面 2 个工具**(为压缩宿主上下文;Spec 0051)。刚升级后若发现以前能用的 `homegraph_node` / `search` / `arkui_migrate` 等「不见了」,不是卸载坏了,而是默认不再暴露——实现仍在,按下方「如何打开更多工具」恢复即可。
177
+
178
+ | 工具 | 用途 |
179
+ |------|------|
180
+ | `homegraph_project` | 工程/模块地图与 Harmony skeleton、资源**路径**导航(无符号正文、无调用图) |
181
+ | `homegraph_explore` | 通用结构探索:相关符号源码、调用路径、usage/依赖/NAPI 关系、route/resource/capability 等图证据 |
177
182
 
178
- **图谱数据源(`--sources` / `HOMEGRAPH_SOURCES`):** 控制 MCP 查询是否使用工程索引、OHOS SDK API 库,或两者。取值 `both`(默认)| `project` | `sdk` | `none`。CLI 优先于环境变量。评测示例:`homegraph serve mcp --path <app> --sources sdk`。`homegraph_status` 会打印当前模式。
183
+ ### 默认不暴露(需配置才进 tools/list)
184
+
185
+ 下列工具 **handler 仍在**,默认不出现在 `tools/list`;未配置时直接调用会返回 `disabled via HOMEGRAPH_MCP_TOOLS`。
179
186
 
180
187
  | 工具 | 用途 |
181
188
  |------|------|
182
- | `homegraph_usages` | **精确用法清单首选**:查询一个已命名 API、成员、常量或字段在哪里被引用 |
183
- | `homegraph_modules` | **模块拓扑首选**:查询已命名模块之间的依赖关系或循环依赖 |
184
- | `homegraph_native` | **Native 边界首选**:查询已命名路径或类型的 NAPI/native 导出和注册位置 |
185
- | `homegraph_explore` | **通用结构探索**:一次调用返回相关符号的源码、调用路径与影响范围;窄清单问题优先使用上面三个专用工具 |
186
- | `homegraph_search` | 按名称快速搜索符号(仅返回位置,不含源码) |
187
- | `homegraph_node` | 读取单个符号或整个文件的源码(带行号)及调用关系;可替代 Read 读文件 |
189
+ | `homegraph_usages` | 精确用法清单:已命名 API / 成员 / 常量 / 字段的引用位置 |
190
+ | `homegraph_modules` | 模块拓扑:已命名模块之间的依赖或循环依赖 |
191
+ | `homegraph_native` | Native 边界:NAPI / native 导出与注册位置 |
192
+ | `homegraph_search` | 按名称快速搜索符号(仅位置,不含源码) |
193
+ | `homegraph_node` | 读取单个符号或整个文件的源码(带行号)及调用关系 |
188
194
  | `homegraph_callers` / `homegraph_callees` | 查看调用方 / 被调用方 |
189
195
  | `homegraph_impact` | 变更影响分析(重构前使用) |
190
196
  | `homegraph_diff_impact` | 传入 unified diff(或 hunks),返回与变更行相交的符号及调用/影响证据包(代码审查用) |
191
197
  | `homegraph_arkui_migrate` | 一次返回 ArkUI 组件迁移/状态语义快照(装饰器、状态字段、Provide/Consume 等) |
192
198
  | `homegraph_files` | 已索引的文件树(支持 glob 过滤、按语言分组) |
193
199
  | `homegraph_status` | 索引健康状态(调试用;含 WAL 大小等) |
194
- | `homegraph_spec_match` | 将新需求描述与 Commit4Spec 知识图谱做全文匹配,返回相似历史Spec及关联提交与代码片段 |
195
- | `homegraph_spec_find` | 根据文件路径反向查找关联的Spec |
196
- | `homegraph_spec_trace` | 根据代码符号追溯回关联的Spec |
200
+ | `homegraph_spec_match` | 将新需求描述与 Commit4Spec 知识图谱做全文匹配 |
201
+ | `homegraph_spec_find` | 根据文件路径反向查找关联的 Spec |
202
+ | `homegraph_spec_trace` | 根据代码符号追溯回关联的 Spec |
203
+
204
+ 默认两件套下:用法/依赖/NAPI 类问题优先用 `homegraph_explore`;读已知路径用宿主 Read/Grep;ArkUI 迁移请显式打开 `arkui_migrate`(或 `all`)。
205
+
206
+ ### 如何打开更多 / 全部 MCP 工具
207
+
208
+ 在 **MCP 服务配置的 `environment` 字段**里设置(与 `HOMEGRAPH_NO_DAEMON` 同级)。这是注入给 HomeGraph MCP **进程**的环境变量;**不要**指望只在 Windows「系统属性 → 环境变量」里添加——MCP 子进程通常读不到系统/用户变量。
209
+
210
+ **打开全部工具:**
211
+
212
+ ```json
213
+ {
214
+ "mcpServers": {
215
+ "homegraph": {
216
+ "command": "homegraph",
217
+ "args": ["serve", "mcp"],
218
+ "environment": {
219
+ "HOMEGRAPH_NO_DAEMON": "1",
220
+ "HOMEGRAPH_MCP_TOOLS": "all"
221
+ }
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ `HOMEGRAPH_MCP_TOOLS=*` 与 `all` 等价。
228
+
229
+ **只打开需要的子集(更省上下文):**
230
+
231
+ ```json
232
+ "environment": {
233
+ "HOMEGRAPH_MCP_TOOLS": "explore,project,node,search,arkui_migrate"
234
+ }
235
+ ```
236
+
237
+ 短名即可(`node` ≡ `homegraph_node`),逗号分隔。改完后**重启对应 Agent / MCP 服务**使配置生效。
238
+
239
+ **小仓注意:** 仅当设为 `all` / `*` 且索引文件数 **少于 500** 时,仍可能再裁到历史 TINY 核心集(`explore` / `search` / `node` / `diff_impact` / `project`)。小仓也要 `usages` / `arkui_migrate` 等时,请用**显式逗号列表**,不要只靠 `all`。
240
+
241
+ **跨项目查询:** 所有工具均支持可选参数 `projectPath`(绝对路径),用于在 monorepo 中查询子项目,或当 MCP 服务器根目录没有索引时指定目标项目。
242
+
243
+ **图谱数据源(`--sources` / `HOMEGRAPH_SOURCES`):** 控制 MCP 查询是否使用工程索引、OHOS SDK API 库,或两者。取值 `both`(默认)| `project` | `sdk` | `none`。CLI 优先于环境变量。评测示例:`homegraph serve mcp --path <app> --sources sdk`。`homegraph_status`(需先暴露该工具)会打印当前模式。
197
244
 
198
245
  ### 查询规划(实验性)
199
246
 
@@ -333,13 +380,16 @@ npm run cli
333
380
  "homegraph": {
334
381
  "type": "stdio",
335
382
  "command": "homegraph",
336
- "args": ["serve", "mcp"]
383
+ "args": ["serve", "mcp"],
384
+ "environment": {
385
+ "HOMEGRAPH_MCP_TOOLS": "all"
386
+ }
337
387
  }
338
388
  }
339
389
  }
340
390
  ```
341
391
 
342
- Cursor 等项目级配置写入 `./.cursor/mcp.json`,格式相同。推荐使用 `homegraph install` 自动完成。
392
+ 不设 `HOMEGRAPH_MCP_TOOLS` 时默认只暴露 `explore` + `project`;需要全量或子集时按上一节「如何打开更多 / 全部 MCP 工具」配置。Cursor 等项目级配置写入 `./.cursor/mcp.json`,格式相同。推荐使用 `homegraph install` 自动完成。
343
393
 
344
394
  ---
345
395
 
@@ -1917,7 +1917,7 @@ function main() {
1917
1917
  */
1918
1918
  program
1919
1919
  .command('install')
1920
- .description('Install homegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent)')
1920
+ .description('Install homegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent, iCode)')
1921
1921
  .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
1922
1922
  .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
1923
1923
  .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
@@ -1976,7 +1976,7 @@ function main() {
1976
1976
  */
1977
1977
  program
1978
1978
  .command('uninstall')
1979
- .description('Remove homegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent)')
1979
+ .description('Remove homegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, DevEco Code, CodeBuddy, Hermes Agent, iCode)')
1980
1980
  .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "all". Default: all')
1981
1981
  .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
1982
1982
  .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all')
package/dist/db/index.js CHANGED
@@ -329,6 +329,7 @@ class DatabaseConnection {
329
329
  'idx_edges_source_kind',
330
330
  'idx_edges_target_kind',
331
331
  'idx_edges_provenance',
332
+ 'idx_edges_synthesized_by',
332
333
  ];
333
334
  /**
334
335
  * Enter bulk-edge-load mode: drop the non-unique edge indexes so the mass
@@ -7,7 +7,7 @@ import { SqliteDatabase } from './sqlite-adapter';
7
7
  /**
8
8
  * Current schema version
9
9
  */
10
- export declare const CURRENT_SCHEMA_VERSION = 11;
10
+ export declare const CURRENT_SCHEMA_VERSION = 12;
11
11
  /**
12
12
  * Migration definition
13
13
  */
@@ -16,6 +16,11 @@ interface Migration {
16
16
  description: string;
17
17
  up: (db: SqliteDatabase) => void;
18
18
  }
19
+ /**
20
+ * Copy `metadata.synthesizedBy` onto `edges.synthesized_by` when the column is empty.
21
+ * Channel deletes read the column, not JSON text (Spec 0053).
22
+ */
23
+ export declare function backfillEdgeSynthesizedBy(db: SqliteDatabase): void;
19
24
  /**
20
25
  * Get the current schema version from the database
21
26
  */
@@ -6,6 +6,7 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.CURRENT_SCHEMA_VERSION = void 0;
9
+ exports.backfillEdgeSynthesizedBy = backfillEdgeSynthesizedBy;
9
10
  exports.getCurrentVersion = getCurrentVersion;
10
11
  exports.runMigrations = runMigrations;
11
12
  exports.needsMigration = needsMigration;
@@ -14,7 +15,7 @@ exports.getMigrationHistory = getMigrationHistory;
14
15
  /**
15
16
  * Current schema version
16
17
  */
17
- exports.CURRENT_SCHEMA_VERSION = 11;
18
+ exports.CURRENT_SCHEMA_VERSION = 12;
18
19
  /**
19
20
  * All migrations in order
20
21
  *
@@ -175,7 +176,31 @@ const migrations = [
175
176
  `);
176
177
  },
177
178
  },
179
+ {
180
+ version: 12,
181
+ description: 'Add edges.synthesized_by and backfill from metadata.synthesizedBy (Spec 0053)',
182
+ up: (db) => {
183
+ const cols = db.prepare('PRAGMA table_info(edges)').all();
184
+ if (!cols.some((c) => c.name === 'synthesized_by')) {
185
+ db.exec('ALTER TABLE edges ADD COLUMN synthesized_by TEXT');
186
+ }
187
+ backfillEdgeSynthesizedBy(db);
188
+ db.exec('CREATE INDEX IF NOT EXISTS idx_edges_synthesized_by ON edges(synthesized_by) WHERE synthesized_by IS NOT NULL');
189
+ },
190
+ },
178
191
  ];
192
+ /**
193
+ * Copy `metadata.synthesizedBy` onto `edges.synthesized_by` when the column is empty.
194
+ * Channel deletes read the column, not JSON text (Spec 0053).
195
+ */
196
+ function backfillEdgeSynthesizedBy(db) {
197
+ db.exec(`
198
+ UPDATE edges
199
+ SET synthesized_by = json_extract(metadata, '$.synthesizedBy')
200
+ WHERE (synthesized_by IS NULL OR synthesized_by = '')
201
+ AND json_extract(metadata, '$.synthesizedBy') IS NOT NULL
202
+ `);
203
+ }
179
204
  /**
180
205
  * Get the current schema version from the database
181
206
  */
@@ -5,6 +5,11 @@
5
5
  */
6
6
  import { SqliteDatabase } from './sqlite-adapter';
7
7
  import { Node, Edge, FileRecord, UnresolvedReference, NodeKind, EdgeKind, Language, GraphStats, SearchOptions, SearchResult } from '../types';
8
+ /**
9
+ * Convert database row to Edge object
10
+ */
11
+ /** Channel name stored on `edges.synthesized_by` (Spec 0053). */
12
+ export declare function edgeSynthesizedBy(edge: Edge): string | null;
8
13
  /**
9
14
  * Query builder for the knowledge graph database
10
15
  */
@@ -46,6 +51,10 @@ export declare class QueryBuilder {
46
51
  /** Attach a versioned OHOS API db for federated symbol lookup. */
47
52
  attachOhosApiDb(dbPath: string | null): void;
48
53
  getOhosApiDbPath(): string | null;
54
+ /** Exact imported SDK module only. No cross-module bare-name fallback.
55
+ * A missing/oversized/ambiguous module returns no claim of signature coverage.
56
+ */
57
+ getOhosApiModuleNodes(specifier: string): Node[];
49
58
  private hasOhosApiAttached;
50
59
  private mapOhosApiNodeRow;
51
60
  /** Set the normalized project-name tokens used to down-weight non-discriminative
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.QueryBuilder = void 0;
42
+ exports.edgeSynthesizedBy = edgeSynthesizedBy;
42
43
  const fs = __importStar(require("fs"));
43
44
  const path = __importStar(require("path"));
44
45
  const utils_1 = require("../utils");
@@ -110,6 +111,11 @@ function rowToNode(row) {
110
111
  /**
111
112
  * Convert database row to Edge object
112
113
  */
114
+ /** Channel name stored on `edges.synthesized_by` (Spec 0053). */
115
+ function edgeSynthesizedBy(edge) {
116
+ const value = edge.metadata?.synthesizedBy;
117
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
118
+ }
113
119
  function rowToEdge(row) {
114
120
  return {
115
121
  source: row.source,
@@ -247,6 +253,21 @@ class QueryBuilder {
247
253
  getOhosApiDbPath() {
248
254
  return this.ohosApiDbPath;
249
255
  }
256
+ /** Exact imported SDK module only. No cross-module bare-name fallback.
257
+ * A missing/oversized/ambiguous module returns no claim of signature coverage.
258
+ */
259
+ getOhosApiModuleNodes(specifier) {
260
+ if (!this.ohosApiDbPath || !/^@(ohos|hms|kit)\.[A-Za-z0-9_.]+$/.test(specifier))
261
+ return [];
262
+ const stem = specifier.startsWith('@kit.') ? specifier.slice(5) : specifier;
263
+ const escaped = stem.replace(/[\\%_]/g, c => `\\${c}`);
264
+ const rows = this.db.prepare(`SELECT * FROM ohos_api.nodes WHERE
265
+ file_path = ? OR file_path = ? OR file_path LIKE ? ESCAPE '\\' OR file_path LIKE ? ESCAPE '\\'
266
+ ORDER BY file_path, start_line LIMIT 257`).all(`${stem}.d.ts`, `${stem}.d.ets`, `%/${escaped}.d.ts`, `%/${escaped}.d.ets`);
267
+ if (rows.length > 256 || new Set(rows.map(row => row.file_path)).size !== 1)
268
+ return [];
269
+ return rows.map(row => this.mapOhosApiNodeRow(row));
270
+ }
250
271
  hasOhosApiAttached() {
251
272
  return this.ohosApiDbPath != null;
252
273
  }
@@ -368,9 +389,10 @@ class QueryBuilder {
368
389
  edge.line ?? null,
369
390
  edge.column ?? null,
370
391
  edge.provenance ?? null,
392
+ edgeSynthesizedBy(edge),
371
393
  ]);
372
394
  }
373
- this.runBatched('insertEdges', 'INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance) VALUES ', '(?,?,?,?,?,?,?)', rows);
395
+ this.runBatched('insertEdges', 'INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance, synthesized_by) VALUES ', '(?,?,?,?,?,?,?,?)', rows);
374
396
  }
375
397
  if (bundle.refs.length > 0)
376
398
  this.insertUnresolvedRefsBatch(bundle.refs);
@@ -979,11 +1001,13 @@ class QueryBuilder {
979
1001
  return [];
980
1002
  return this.mergeOhosApiTextSearch(text, { kinds, languages, limit }, []);
981
1003
  }
982
- // First try FTS5 with prefix matching
1004
+ // First try FTS5 with prefix matching. Chinese literals (for example
1005
+ // string.json values in docstring) are real FTS tokens — do not skip them.
983
1006
  let results = text
984
1007
  ? this.searchNodesFTS(text, { kinds, languages, limit, offset })
985
1008
  // Over-fetch by 5× when running filter-only (no text). The
986
- // post-scoring path: + name: filters can be very selective, so // a smaller multiplier risks returning fewer than `limit`
1009
+ // post-scoring path: name: filters can be very selective, so
1010
+ // a smaller multiplier risks returning fewer than `limit`
987
1011
  // results despite the DB having plenty of matches.
988
1012
  : this.searchAllByFilters({ kinds, languages, limit: limit * 5 });
989
1013
  // If no FTS results, try LIKE-based substring search
@@ -1482,8 +1506,8 @@ class QueryBuilder {
1482
1506
  insertEdge(edge) {
1483
1507
  if (!this.stmts.insertEdge) {
1484
1508
  this.stmts.insertEdge = this.db.prepare(`
1485
- INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance)
1486
- VALUES (@source, @target, @kind, @metadata, @line, @col, @provenance)
1509
+ INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance, synthesized_by)
1510
+ VALUES (@source, @target, @kind, @metadata, @line, @col, @provenance, @synthesizedBy)
1487
1511
  `);
1488
1512
  }
1489
1513
  this.stmts.insertEdge.run({
@@ -1494,6 +1518,7 @@ class QueryBuilder {
1494
1518
  line: edge.line ?? null,
1495
1519
  col: edge.column ?? null,
1496
1520
  provenance: edge.provenance ?? null,
1521
+ synthesizedBy: edgeSynthesizedBy(edge),
1497
1522
  });
1498
1523
  }
1499
1524
  /**
@@ -1698,8 +1723,8 @@ class QueryBuilder {
1698
1723
  .prepare(`DELETE FROM edges
1699
1724
  WHERE kind = 'calls'
1700
1725
  AND provenance = 'heuristic'
1701
- AND metadata LIKE '%"synthesizedBy":"arkanalyzer"%'
1702
- AND metadata LIKE '%"sourceFile"%'`)
1726
+ AND synthesized_by = 'arkanalyzer'
1727
+ AND json_extract(metadata, '$.sourceFile') IS NOT NULL`)
1703
1728
  .run();
1704
1729
  return result.changes;
1705
1730
  }
@@ -52,6 +52,9 @@ CREATE TABLE IF NOT EXISTS edges (
52
52
  line INTEGER,
53
53
  col INTEGER,
54
54
  provenance TEXT DEFAULT NULL,
55
+ -- Channel of a heuristic edge (`metadata.synthesizedBy`). Written on insert
56
+ -- so re-index can delete by channel without matching JSON text (Spec 0053).
57
+ synthesized_by TEXT,
55
58
  FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE,
56
59
  FOREIGN KEY (target) REFERENCES nodes(id) ON DELETE CASCADE
57
60
  );
@@ -192,6 +195,7 @@ CREATE INDEX IF NOT EXISTS idx_unresolved_from_name ON unresolved_refs(from_node
192
195
  CREATE INDEX IF NOT EXISTS idx_unresolved_status ON unresolved_refs(status);
193
196
  CREATE INDEX IF NOT EXISTS idx_unresolved_failed_tail ON unresolved_refs(name_tail) WHERE status = 'failed';
194
197
  CREATE INDEX IF NOT EXISTS idx_edges_provenance ON edges(provenance);
198
+ CREATE INDEX IF NOT EXISTS idx_edges_synthesized_by ON edges(synthesized_by) WHERE synthesized_by IS NOT NULL;
195
199
 
196
200
  -- Project metadata for version/provenance tracking
197
201
  CREATE TABLE IF NOT EXISTS project_metadata (
@@ -529,7 +529,8 @@ function looksLikeCpp(source) {
529
529
  // routed through the C extractor (which extracts no classes), and its class
530
530
  // definition silently vanishes. The two-token shape (`<KW> <MACRO> <Name>`
531
531
  // before a `[:{]`) never occurs in valid C, so this can't misclassify C headers.
532
- return /\bnamespace\b|\bclass\s+\w+\s*[:{]|\b(?:class|struct)\s+[A-Z][A-Z0-9_]+\s+\w+\s*(?:final\s*)?[:{]|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample);
532
+ // `struct Derived : Base` is C++. A plain `struct Point {` stays C.
533
+ return /\bnamespace\b|\bclass\s+\w+\s*[:{]|\bstruct\s+\w+\s*:|\b(?:class|struct)\s+[A-Z][A-Z0-9_]+\s+\w+\s*(?:final\s*)?[:{]|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample);
533
534
  }
534
535
  /**
535
536
  * Heuristic: does a .h file contain Objective-C constructs?
@@ -102,6 +102,14 @@ export declare function blankMetalAttributes(source: string): string;
102
102
  * ALL-CAPS call that isn't markup only loses that one annotation, never a whole
103
103
  * class.
104
104
  */
105
+ /**
106
+ * Mask C++ raw string literals (`R"delim(...)delim"`) so later blankers do not
107
+ * treat their contents as code. Offsets stay aligned with the original source.
108
+ */
109
+ export declare function maskCppRawStrings(source: string): {
110
+ source: string;
111
+ restore: (blanked: string) => string;
112
+ };
105
113
  export declare function blankCppAnnotationMacroCalls(source: string): string;
106
114
  export declare function blankCppApiPrefixMacros(source: string): string;
107
115
  export declare function blankCppInlineAnnotationMacros(source: string): string;
@@ -7,6 +7,7 @@ exports.blankCppExportMacros = blankCppExportMacros;
7
7
  exports.blankCppInlineMacros = blankCppInlineMacros;
8
8
  exports.recoverMangledCppName = recoverMangledCppName;
9
9
  exports.blankMetalAttributes = blankMetalAttributes;
10
+ exports.maskCppRawStrings = maskCppRawStrings;
10
11
  exports.blankCppAnnotationMacroCalls = blankCppAnnotationMacroCalls;
11
12
  exports.blankCppApiPrefixMacros = blankCppApiPrefixMacros;
12
13
  exports.blankCppInlineAnnotationMacros = blankCppInlineAnnotationMacros;
@@ -493,6 +494,49 @@ function blankMetalAttributes(source) {
493
494
  * ALL-CAPS call that isn't markup only loses that one annotation, never a whole
494
495
  * class.
495
496
  */
497
+ /**
498
+ * Mask C++ raw string literals (`R"delim(...)delim"`) so later blankers do not
499
+ * treat their contents as code. Offsets stay aligned with the original source.
500
+ */
501
+ function maskCppRawStrings(source) {
502
+ const unchanged = { source, restore: (blanked) => blanked };
503
+ if (source.indexOf('R"') === -1)
504
+ return unchanged;
505
+ const re = /\/\/[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)|\b(?:u8|[LuU])?R"([^ \t\v\f\r\n()\\]{0,16})\(|"(?:\\[\s\S]|[^"\\])*(?:"|$)|(?<!\w)(?:u8|[LuU])?'(?:\\[\s\S]|[^'\\])*(?:'|$)/g;
506
+ const spans = [];
507
+ const parts = [];
508
+ let last = 0;
509
+ let m;
510
+ while ((m = re.exec(source)) !== null) {
511
+ if (m[1] === undefined)
512
+ continue;
513
+ const closer = `)${m[1]}"`;
514
+ const close = source.indexOf(closer, re.lastIndex);
515
+ const end = close < 0 ? source.length : close + closer.length;
516
+ spans.push({ start: m.index, end });
517
+ parts.push(source.slice(last, m.index), source.slice(m.index, end).replace(/[^\r\n]/g, '\0'));
518
+ last = re.lastIndex = end;
519
+ }
520
+ if (spans.length === 0)
521
+ return unchanged;
522
+ parts.push(source.slice(last));
523
+ const masked = parts.join('');
524
+ return {
525
+ source: masked,
526
+ restore(blanked) {
527
+ if (blanked === masked || blanked.length !== source.length)
528
+ return source;
529
+ const chars = blanked.split('');
530
+ for (const { start, end } of spans) {
531
+ for (let i = start; i < end; i++) {
532
+ if (chars[i] === '\0')
533
+ chars[i] = source[i];
534
+ }
535
+ }
536
+ return chars.join('');
537
+ },
538
+ };
539
+ }
496
540
  function blankCppAnnotationMacroCalls(source) {
497
541
  if (!/^[ \t]*[A-Z][A-Z0-9_]{2,}\s*\(/m.test(source))
498
542
  return source;
@@ -44,5 +44,9 @@ export declare function buildRelaunchArgv(scriptPath: string, scriptArgs: readon
44
44
  * On spawn failure, returns so the caller runs in-process anyway — risking the
45
45
  * OOM is still better than refusing to start.
46
46
  */
47
+ /** Bun is not V8 — `--liftoff-only` / `--max-old-space-size` relaunch does not apply. */
48
+ export declare function isBunRuntime(g?: {
49
+ Bun?: unknown;
50
+ }): boolean;
47
51
  export declare function relaunchWithWasmRuntimeFlagsIfNeeded(scriptPath: string): void;
48
52
  //# sourceMappingURL=wasm-runtime-flags.d.ts.map
@@ -6,6 +6,7 @@ exports.maxOldSpaceSizeFlag = maxOldSpaceSizeFlag;
6
6
  exports.processHasWasmRuntimeFlags = processHasWasmRuntimeFlags;
7
7
  exports.processHasRequiredRuntimeFlags = processHasRequiredRuntimeFlags;
8
8
  exports.buildRelaunchArgv = buildRelaunchArgv;
9
+ exports.isBunRuntime = isBunRuntime;
9
10
  exports.relaunchWithWasmRuntimeFlagsIfNeeded = relaunchWithWasmRuntimeFlagsIfNeeded;
10
11
  /**
11
12
  * WASM runtime flags — workaround for the V8 turboshaft WASM Zone OOM.
@@ -134,6 +135,10 @@ function buildRelaunchArgv(scriptPath, scriptArgs, execArgv = process.execArgv)
134
135
  * On spawn failure, returns so the caller runs in-process anyway — risking the
135
136
  * OOM is still better than refusing to start.
136
137
  */
138
+ /** Bun is not V8 — `--liftoff-only` / `--max-old-space-size` relaunch does not apply. */
139
+ function isBunRuntime(g = globalThis) {
140
+ return typeof g.Bun !== 'undefined';
141
+ }
137
142
  function relaunchWithWasmRuntimeFlagsIfNeeded(scriptPath) {
138
143
  if (processHasRequiredRuntimeFlags())
139
144
  return;
@@ -141,6 +146,10 @@ function relaunchWithWasmRuntimeFlagsIfNeeded(scriptPath) {
141
146
  return;
142
147
  if (process.env.HOMEGRAPH_NO_RELAUNCH)
143
148
  return;
149
+ // Product hosts (e.g. DevEco Code) spawn MCP under Bun; V8 flags would be
150
+ // ignored or break relaunch. Stay in-process.
151
+ if (isBunRuntime())
152
+ return;
144
153
  const argv = buildRelaunchArgv(scriptPath, process.argv.slice(2));
145
154
  const result = (0, child_process_1.spawnSync)(process.execPath, argv, {
146
155
  stdio: 'inherit',
@@ -53,6 +53,8 @@ export declare function isEvidencePathNode(n: Node): boolean;
53
53
  export declare function completeEvidencePathCandidates(query: string, nodes: Node[], lookup: (qualifiedName: string, limit: number) => Node[], plan?: QueryPlan): Node[];
54
54
  /** Resolve only actual candidate identities; a bare homonym never selects the first hit. */
55
55
  export declare function resolveEvidencePathGoal(query: string, nodes: Node[], focusIds: Set<string>, plan?: QueryPlan): EvidencePathGoal;
56
+ /** Framework-labelled static UI wiring; never treat an arbitrary reference as rendering. */
57
+ export declare function isArkuiScopeEdge(edge: Edge): boolean;
56
58
  export declare function evidenceEdgeCost(edge: Edge, goal: EvidencePathGoal): number | null;
57
59
  export declare function evidenceEdgeKey(edge: Edge): string;
58
60
  /** One bounded search per request. Each orientation follows directed edges throughout. */
@@ -4,6 +4,7 @@ exports.EVIDENCE_PATH_LIMITS = void 0;
4
4
  exports.isEvidencePathNode = isEvidencePathNode;
5
5
  exports.completeEvidencePathCandidates = completeEvidencePathCandidates;
6
6
  exports.resolveEvidencePathGoal = resolveEvidencePathGoal;
7
+ exports.isArkuiScopeEdge = isArkuiScopeEdge;
7
8
  exports.evidenceEdgeCost = evidenceEdgeCost;
8
9
  exports.evidenceEdgeKey = evidenceEdgeKey;
9
10
  exports.searchEvidencePaths = searchEvidencePaths;
@@ -108,11 +109,18 @@ function edgeSemantics(edge) {
108
109
  || ['callback', 'event-emitter', 'closure-collection', 'fn-pointer-dispatch', 'arkui-emitter', 'arkui-common-event', 'arkui-taskpool'].includes(String(by));
109
110
  return { state, event };
110
111
  }
112
+ /** Framework-labelled static UI wiring; never treat an arbitrary reference as rendering. */
113
+ function isArkuiScopeEdge(edge) {
114
+ const by = edge.metadata?.synthesizedBy;
115
+ return (edge.kind === 'references' || edge.kind === 'calls' || edge.kind === 'contains') && ((by === 'viewtree' && ['child-component', 'builder', 'builder-param'].includes(String(edge.metadata?.via)))
116
+ || (by === 'arkui-ownership' && edge.metadata?.via === 'build') || by === 'arkui-child' || by === 'arkui-named-nav' || by === 'arkts-route-map' || by === 'arkui-route');
117
+ }
111
118
  function evidenceEdgeCost(edge, goal) {
112
119
  const { state, event } = edgeSemantics(edge);
113
120
  let relevanceCost;
114
121
  if (goal.kind === 'structure') {
115
- if (!['extends', 'implements', 'overrides', 'instantiates'].includes(edge.kind))
122
+ if (!['extends', 'implements', 'overrides', 'instantiates'].includes(edge.kind)
123
+ && !(process.env.HOMEGRAPH_ARKTS_IMPLEMENTATION_CONTEXT !== '0' && isArkuiScopeEdge(edge)))
116
124
  return null;
117
125
  relevanceCost = 0;
118
126
  }
@@ -143,7 +151,7 @@ function searchEvidencePaths(graph, goal, options = {}) {
143
151
  const neighbors = new Map();
144
152
  const paths = [];
145
153
  const missing = [];
146
- const kinds = goal.kind === 'structure' ? ['extends', 'implements', 'overrides', 'instantiates'] : ['calls', 'references'];
154
+ const kinds = goal.kind === 'structure' ? ['extends', 'implements', 'overrides', 'instantiates', ...(process.env.HOMEGRAPH_ARKTS_IMPLEMENTATION_CONTEXT !== '0' ? ['calls', 'references', 'contains'] : [])] : ['calls', 'references'];
147
155
  const alive = () => {
148
156
  if (now() - started >= limits.milliseconds) {
149
157
  hits.add('time');
@@ -179,7 +187,16 @@ function searchEvidencePaths(graph, goal, options = {}) {
179
187
  stats.rowsRead += rows.length;
180
188
  if (rows.length > limits.perNode)
181
189
  hits.add('neighbors');
182
- const valid = rows.slice(0, limits.perNode).filter(e => evidenceEdgeCost(e, goal) !== null
190
+ const valid = rows.slice(0, limits.perNode).map(e => {
191
+ if (goal.kind === 'structure' && e.kind === 'contains' && process.env.HOMEGRAPH_ARKTS_IMPLEMENTATION_CONTEXT !== '0') {
192
+ const from = node(e.source);
193
+ const to = node(e.target);
194
+ if (from && ['component', 'struct'].includes(from.kind) && to?.kind === 'method' && to.name === 'build') {
195
+ return { ...e, metadata: { ...e.metadata, synthesizedBy: 'arkui-ownership', via: 'build' } };
196
+ }
197
+ }
198
+ return e;
199
+ }).filter(e => evidenceEdgeCost(e, goal) !== null
183
200
  && node(e.source) && node(e.target)).sort((a, b) => evidenceEdgeCost(a, goal) - evidenceEdgeCost(b, goal)
184
201
  || evidenceEdgeKey(a).localeCompare(evidenceEdgeKey(b)));
185
202
  adjacency.set(key, valid);
package/dist/index.js CHANGED
@@ -854,7 +854,13 @@ class HomeGraph {
854
854
  }
855
855
  const filesChanged = result.filesAdded + result.filesModified + result.filesRemoved;
856
856
  return { filesChanged, durationMs: result.durationMs };
857
- }, options);
857
+ }, {
858
+ ...options,
859
+ indexedFingerprint: options.indexedFingerprint ?? ((rel) => {
860
+ const rec = this.getFile(rel);
861
+ return rec ? { size: rec.size, modifiedAt: rec.modifiedAt } : null;
862
+ }),
863
+ });
858
864
  return this.watcher.start();
859
865
  }
860
866
  /**