homegraph 1.6.0 → 1.6.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/CHANGELOG.md CHANGED
@@ -10,6 +10,13 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10
10
  ## [Unreleased]
11
11
 
12
12
 
13
+ ## [1.6.1] - 2026-09-28
14
+
15
+ ### Improvements
16
+
17
+ - 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).
18
+ - 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).
19
+
13
20
  ## [1.6.0] - 2026-09-23
14
21
 
15
22
  ### Improvements
@@ -38,6 +45,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
38
45
 
39
46
  ### Improvements
40
47
 
48
+ - 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
49
  - Bump `arkanalyzer` to **1.0.94**.
42
50
  - 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
51
  - 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 +282,4 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
274
282
  [1.5.8]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.5.8
275
283
  [1.5.10]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.5.10
276
284
  [1.6.0]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.0
285
+ [1.6.1]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.1
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
 
@@ -46,6 +46,10 @@ export declare class QueryBuilder {
46
46
  /** Attach a versioned OHOS API db for federated symbol lookup. */
47
47
  attachOhosApiDb(dbPath: string | null): void;
48
48
  getOhosApiDbPath(): string | null;
49
+ /** Exact imported SDK module only. No cross-module bare-name fallback.
50
+ * A missing/oversized/ambiguous module returns no claim of signature coverage.
51
+ */
52
+ getOhosApiModuleNodes(specifier: string): Node[];
49
53
  private hasOhosApiAttached;
50
54
  private mapOhosApiNodeRow;
51
55
  /** Set the normalized project-name tokens used to down-weight non-discriminative
@@ -247,6 +247,21 @@ class QueryBuilder {
247
247
  getOhosApiDbPath() {
248
248
  return this.ohosApiDbPath;
249
249
  }
250
+ /** Exact imported SDK module only. No cross-module bare-name fallback.
251
+ * A missing/oversized/ambiguous module returns no claim of signature coverage.
252
+ */
253
+ getOhosApiModuleNodes(specifier) {
254
+ if (!this.ohosApiDbPath || !/^@(ohos|hms|kit)\.[A-Za-z0-9_.]+$/.test(specifier))
255
+ return [];
256
+ const stem = specifier.startsWith('@kit.') ? specifier.slice(5) : specifier;
257
+ const escaped = stem.replace(/[\\%_]/g, c => `\\${c}`);
258
+ const rows = this.db.prepare(`SELECT * FROM ohos_api.nodes WHERE
259
+ file_path = ? OR file_path = ? OR file_path LIKE ? ESCAPE '\\' OR file_path LIKE ? ESCAPE '\\'
260
+ ORDER BY file_path, start_line LIMIT 257`).all(`${stem}.d.ts`, `${stem}.d.ets`, `%/${escaped}.d.ts`, `%/${escaped}.d.ets`);
261
+ if (rows.length > 256 || new Set(rows.map(row => row.file_path)).size !== 1)
262
+ return [];
263
+ return rows.map(row => this.mapOhosApiNodeRow(row));
264
+ }
250
265
  hasOhosApiAttached() {
251
266
  return this.ohosApiDbPath != null;
252
267
  }
@@ -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);
@@ -1,3 +1,6 @@
1
+ import { type SdkModule } from './implementation-context';
2
+ import { type RequestContract } from '../search/request-contract';
3
+ import { type RequestEvidence, type ControlEvidence } from './request-evidence';
1
4
  import type HomeGraph from '../index';
2
5
  import type { Edge, Node } from '../types';
3
6
  import type { ExploreEmission } from './explore-session-state';
@@ -22,6 +25,15 @@ interface EvidencePack {
22
25
  sources: string[];
23
26
  priority: number;
24
27
  gaps: EvidenceGap[];
28
+ context?: {
29
+ label: string;
30
+ notes: string[];
31
+ sdk?: {
32
+ module: string;
33
+ version: string;
34
+ nodes: Node[];
35
+ };
36
+ };
25
37
  relation?: RelationEvidence;
26
38
  path?: {
27
39
  id: string;
@@ -36,6 +48,9 @@ export interface ArktsEvidenceResult {
36
48
  version: 1 | 2;
37
49
  scope: 'bounded_static_evidence';
38
50
  status: 'complete' | 'partial' | 'empty';
51
+ requestEvidence?: RequestEvidence;
52
+ controls?: ControlEvidence[];
53
+ implementationContext?: Array<NonNullable<EvidencePack['context']>>;
39
54
  selectedPacks: string[];
40
55
  gaps: EvidenceGap[];
41
56
  relations: NonNullable<EvidencePack['relation']>[];
@@ -62,6 +77,8 @@ export declare function buildArktsEvidencePacks(graph: Pick<HomeGraph, 'getNode'
62
77
  maxChars: number;
63
78
  maxFiles?: number;
64
79
  pathSearch?: EvidencePathSearch;
80
+ requestContract?: RequestContract;
81
+ sdkModule?: (module: string) => SdkModule | undefined;
65
82
  }): ArktsEvidenceResult | null;
66
83
  export {};
67
84
  //# sourceMappingURL=arkts-evidence-packs.d.ts.map
@@ -1,6 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.buildArktsEvidencePacks = buildArktsEvidencePacks;
37
+ const arkts_1 = require("../extraction/languages/arkts");
38
+ const implementation_context_1 = require("./implementation-context");
39
+ const path = __importStar(require("node:path"));
40
+ const request_contract_1 = require("../search/request-contract");
41
+ const request_evidence_1 = require("./request-evidence");
4
42
  const node_crypto_1 = require("node:crypto");
5
43
  const node_fs_1 = require("node:fs");
6
44
  const utils_1 = require("../utils");
@@ -8,7 +46,7 @@ const evidence_rendering_1 = require("./evidence-rendering");
8
46
  const explore_dedup_1 = require("./explore-dedup");
9
47
  const evidence_paths_1 = require("../graph/evidence-paths");
10
48
  const LIMITS = { nodes: 36, expand: 12, edges: 48, files: 8, fileBytes: 512 * 1024, bytes: 2 * 1024 * 1024 };
11
- const DECLARATIONS = new Set(['class', 'struct', 'component', 'interface', 'method', 'function', 'property', 'field', 'constant', 'variable', 'enum']);
49
+ const BASE_DECLARATIONS = new Set(['class', 'struct', 'component', 'interface', 'method', 'function', 'property', 'field', 'constant', 'variable', 'enum']);
12
50
  const CONTAINERS = new Set(['class', 'struct', 'component', 'interface', 'enum']);
13
51
  const RELATIONS = new Set(['calls', 'references', 'extends', 'implements', 'instantiates']);
14
52
  const inline = (s) => s.replace(/[`\r\n]/g, ' ');
@@ -22,16 +60,32 @@ const artifact = (n) => n.filePath.includes('@dummy') || n.name.startsWith('@dum
22
60
  || (n.name === 'constructor' && n.startLine === 1 && n.endLine === 1);
23
61
  /** Consume already-located nodes. No text search, new planner call or recursive retrieval. */
24
62
  function buildArktsEvidencePacks(graph, options) {
63
+ const contextEnabled = (0, implementation_context_1.implementationContextEnabled)();
64
+ const DECLARATIONS = contextEnabled ? new Set([...BASE_DECLARATIONS, 'type_alias', 'import', 'export', 'route', 'file']) : BASE_DECLARATIONS;
25
65
  if (!options.nodes.some(n => local(n) && /\.ets$/i.test(n.filePath) && DECLARATIONS.has(n.kind)))
26
66
  return null;
27
67
  const search = options.pathSearch;
68
+ const targetChecks = (0, request_contract_1.accuracyTargetsEnabled)();
69
+ const behaviorChecks = (0, request_contract_1.accuracyCoverageEnabled)();
70
+ const inspector = options.requestContract && (targetChecks || behaviorChecks)
71
+ ? (0, request_evidence_1.createRequestEvidenceInspector)(options.requestContract) : undefined;
72
+ const requestSources = new Map();
73
+ const requestUnit = (s) => {
74
+ let unit = requestSources.get(s);
75
+ if (!unit) {
76
+ unit = { filePath: s.node.filePath, start: s.start, source: s.source };
77
+ requestSources.set(s, unit);
78
+ }
79
+ return unit;
80
+ };
28
81
  const requiredIds = search ? new Set([...search.goal.anchorIds, ...search.paths.flatMap(p => p.nodeIds)]) : options.focusIds;
29
82
  const inputNodes = search && requiredIds.size ? [...requiredIds].flatMap(id => { const n = graph.getNode(id); return n ? [n] : []; }) : options.nodes;
30
83
  const all = (0, evidence_rendering_1.canonicalSourceDeclarations)(inputNodes.filter(n => DECLARATIONS.has(n.kind) && local(n) && !artifact(n)));
31
84
  if (!all.length)
32
85
  return null;
86
+ const inputOrder = new Map(all.map((node, index) => [node.id, index]));
33
87
  const candidates = all.sort((a, b) => Number(requiredIds.has(b.id)) - Number(requiredIds.has(a.id))
34
- || a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine).slice(0, LIMITS.nodes);
88
+ || (inspector && targetChecks ? inputOrder.get(a.id) - inputOrder.get(b.id) : a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine)).slice(0, LIMITS.nodes);
35
89
  const focus = candidates.filter(n => requiredIds.has(n.id));
36
90
  // A named owning type provides scope for its named members, not another
37
91
  // callable endpoint. Do not invent a missing Type→method call obligation.
@@ -46,11 +100,11 @@ function buildArktsEvidencePacks(graph, options) {
46
100
  let bytesRead = 0;
47
101
  const root = (0, node_fs_1.realpathSync)(options.projectRoot);
48
102
  const maxFiles = Math.max(1, Math.min(LIMITS.files, options.maxFiles ?? LIMITS.files));
49
- const readSource = (node) => {
103
+ const readSource = (node, snapshot = false) => {
50
104
  const previous = nodeSources.get(node.id);
51
105
  if (previous)
52
106
  return previous;
53
- if (!local(node) || artifact(node) || !DECLARATIONS.has(node.kind)) {
107
+ if ((!local(node) && !(contextEnabled && (snapshot || node.kind === 'route' || (!node.filePath.startsWith('ohos-sdk:') && /\.d\.(?:ts|ets)$/.test(node.filePath))))) || artifact(node) || !DECLARATIONS.has(node.kind)) {
54
108
  const failure = { gap: { target: label(node), reason: 'unavailable', nextAnchor: location(node) } };
55
109
  nodeSources.set(node.id, failure);
56
110
  return failure;
@@ -90,7 +144,7 @@ function buildArktsEvidencePacks(graph, options) {
90
144
  const content = buffer.subarray(0, bytes).toString('utf8');
91
145
  const indexed = graph.getFile(node.filePath);
92
146
  // Hash equality, not mtime: edits can retain timestamps and invalidate ranges.
93
- if (bytes !== size || !indexed || indexed.contentHash !== (0, node_crypto_1.createHash)('sha256').update(content).digest('hex'))
147
+ if (bytes !== size || (!snapshot && (!indexed || indexed.contentHash !== (0, node_crypto_1.createHash)('sha256').update(content).digest('hex'))))
94
148
  file.reason = 'stale';
95
149
  else
96
150
  file = { content, lines: content.split('\n'), fingerprint: (0, explore_dedup_1.fileFingerprint)(content) };
@@ -102,6 +156,14 @@ function buildArktsEvidencePacks(graph, options) {
102
156
  }
103
157
  fileCache.set(node.filePath, file);
104
158
  }
159
+ if ((snapshot || node.kind === 'file') && file.lines)
160
+ node = { ...node, endLine: file.lines.length };
161
+ if (node.kind === 'import' && file.lines) {
162
+ const head = file.lines.slice(node.startLine - 1, node.startLine + 20).join('\n');
163
+ const importText = head.match(/^\s*import\s+[\s\S]*?\bfrom\s*['"][^'"\r\n]+['"][^\S\r\n]*;?/);
164
+ if (importText)
165
+ node = { ...node, endLine: node.startLine + importText[0].split('\n').length - 1 };
166
+ }
105
167
  const reason = file.reason ?? (!Number.isInteger(node.startLine) || !Number.isInteger(node.endLine)
106
168
  || node.startLine < 1 || node.endLine < node.startLine || node.endLine > file.lines.length
107
169
  || !file.lines.slice(node.startLine - 1, node.endLine).join('\n').trim() ? 'invalid_range' : undefined);
@@ -117,13 +179,13 @@ function buildArktsEvidencePacks(graph, options) {
117
179
  const id = `${node.filePath}:${start}-${node.endLine}`;
118
180
  if (!sources.has(id))
119
181
  sources.set(id, { id, node, start, end: node.endLine,
120
- source: file.lines.slice(start - 1, node.endLine).join('\n'), fingerprint: file.fingerprint });
182
+ source: file.lines.slice(start - 1, node.endLine).join('\n'), fingerprint: file.fingerprint, ...(snapshot ? { snapshot: true } : {}) });
121
183
  const result = { id };
122
184
  nodeSources.set(node.id, result);
123
185
  return result;
124
186
  };
125
187
  const dependencies = (nodes) => {
126
- const resolved = nodes.map(readSource);
188
+ const resolved = nodes.map(n => readSource(n));
127
189
  return { sources: [...new Set(resolved.flatMap(s => s.id ? [s.id] : []))],
128
190
  gaps: resolved.flatMap(s => s.gap ? [s.gap] : []) };
129
191
  };
@@ -134,6 +196,104 @@ function buildArktsEvidencePacks(graph, options) {
134
196
  if (all.length > candidates.length)
135
197
  globalGaps.push({ target: `${all.length - candidates.length} additional candidates`,
136
198
  reason: 'scope_limit', nextAnchor: location(all[candidates.length]) });
199
+ const configNode = (file) => ({ id: `context-config:${file}`, kind: 'constant', name: file, qualifiedName: file,
200
+ filePath: file, startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: 0, language: 'yaml' });
201
+ const manifestCache = new Map();
202
+ const nearestManifest = (file) => {
203
+ if (manifestCache.has(file))
204
+ return manifestCache.get(file);
205
+ let dir = path.posix.dirname(file.replace(/\\/g, '/'));
206
+ let found;
207
+ for (let depth = 0; depth < 12; depth++) {
208
+ const manifest = path.posix.join(dir, 'oh-package.json5');
209
+ const absolute = (0, utils_1.validatePathWithinRoot)(root, manifest);
210
+ try {
211
+ if (absolute && (0, node_fs_1.statSync)(absolute).isFile()) {
212
+ found = configNode(manifest);
213
+ break;
214
+ }
215
+ }
216
+ catch { /* absent */ }
217
+ if (dir === '.')
218
+ break;
219
+ dir = path.posix.dirname(dir);
220
+ }
221
+ manifestCache.set(file, found);
222
+ return found;
223
+ };
224
+ const configValue = (node) => {
225
+ const r = readSource(node, true);
226
+ if (!r.id)
227
+ return undefined;
228
+ try {
229
+ const value = (0, arkts_1.parseJson5Minimal)(sources.get(r.id).source);
230
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
231
+ }
232
+ catch {
233
+ return undefined;
234
+ }
235
+ };
236
+ const moduleBinding = (importer, module) => {
237
+ const manifest = nearestManifest(importer);
238
+ if (!manifest)
239
+ return undefined;
240
+ const config = configValue(manifest);
241
+ const deps = config?.dependencies;
242
+ if (!deps || typeof deps !== 'object' || Array.isArray(deps))
243
+ return undefined;
244
+ const dep = deps[module];
245
+ if (typeof dep !== 'string' || !/^file:\.{1,2}\//.test(dep))
246
+ return undefined;
247
+ const depRoot = path.posix.normalize(path.posix.join(path.posix.dirname(manifest.filePath), dep.slice(5)));
248
+ if (depRoot === '..' || depRoot.startsWith('../') || path.posix.isAbsolute(depRoot))
249
+ return undefined;
250
+ const targetManifest = configNode(path.posix.join(depRoot, 'oh-package.json5'));
251
+ const target = configValue(targetManifest);
252
+ if (!target)
253
+ return undefined;
254
+ const entry = typeof target.main === 'string' ? path.posix.join(depRoot, target.main) : depRoot;
255
+ const relativePath = './' + path.posix.relative(path.posix.dirname(importer), entry);
256
+ return { relativePath, witnesses: [manifest, targetManifest] };
257
+ };
258
+ const contextGroups = contextEnabled ? (0, implementation_context_1.collectImplementationContext)(graph, focus.length ? focus : candidates, node => { const r = readSource(node); return r.id ? sources.get(r.id)?.source : undefined; }, options.sdkModule, moduleBinding) : [];
259
+ for (const group of contextGroups) {
260
+ const deps = dependencies(group.nodes);
261
+ packs.push({ id: group.id, label: group.label, priority: 130, ...deps,
262
+ context: { label: group.label, notes: [...group.notes, ...group.missing.map(m => `Unresolved: ${m}`)], ...(group.sdk ? { sdk: group.sdk } : {}) } });
263
+ for (const missing of group.missing)
264
+ globalGaps.push({ target: missing, reason: 'unindexed_connection', nextAnchor: missing });
265
+ }
266
+ // Only ancestor manifests of the located code; no workspace traversal or credential files.
267
+ const configs = new Set();
268
+ for (const node of contextEnabled && contextGroups.some(g => g.id.startsWith('impl-import:')) ? (focus.length ? focus : candidates).slice(0, 3) : []) {
269
+ let dir = path.posix.dirname(node.filePath.replace(/\\/g, '/'));
270
+ for (let depth = 0; depth < 12; depth++) {
271
+ const manifest = path.posix.join(dir, 'oh-package.json5');
272
+ const absolute = (0, utils_1.validatePathWithinRoot)(root, manifest);
273
+ try {
274
+ if (absolute && (0, node_fs_1.statSync)(absolute).isFile()) {
275
+ configs.add(manifest);
276
+ break;
277
+ }
278
+ }
279
+ catch { /* missing ancestor manifest */ }
280
+ if (dir === '.')
281
+ break;
282
+ dir = path.posix.dirname(dir);
283
+ }
284
+ }
285
+ if (configs.size)
286
+ configs.add('build-profile.json5');
287
+ for (const config of configs) {
288
+ const node = { id: `context-config:${config}`, kind: 'constant', name: config, qualifiedName: config,
289
+ filePath: config, startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: Date.now(), language: 'yaml' };
290
+ const dep = readSource(node, true);
291
+ if (dep.id)
292
+ packs.push({ id: node.id, label: `module/version configuration ${config}`, priority: 125, sources: [dep.id], gaps: [],
293
+ context: { label: `module/version configuration ${config}`, notes: ['Current configuration snapshot; declared dependencies do not prove installation or export availability.'] } });
294
+ else if (dep.gap && dep.gap.reason !== 'unavailable')
295
+ globalGaps.push(dep.gap);
296
+ }
137
297
  const candidateIds = new Set(candidates.map(n => n.id));
138
298
  const edges = [];
139
299
  const edgeKeys = new Set();
@@ -158,6 +318,16 @@ function buildArktsEvidencePacks(graph, options) {
158
318
  edges.push(edge);
159
319
  }
160
320
  }
321
+ for (const group of contextGroups)
322
+ for (const edge of group.edges) {
323
+ if (edge.kind === 'imports')
324
+ continue;
325
+ const key = (0, evidence_paths_1.evidenceEdgeKey)(edge);
326
+ if (!edgeKeys.has(key)) {
327
+ edgeKeys.add(key);
328
+ edges.push(edge);
329
+ }
330
+ }
161
331
  if (search) {
162
332
  const fallbackNeighbors = search.missing.length || search.goal.anchorIds.length < 2 ? search.neighbors : [];
163
333
  for (const edge of [...search.paths.flatMap(p => p.edges), ...fallbackNeighbors]) {
@@ -194,7 +364,7 @@ function buildArktsEvidencePacks(graph, options) {
194
364
  for (const [i, edge] of edges.entries()) {
195
365
  const from = graph.getNode(edge.source);
196
366
  const to = graph.getNode(edge.target);
197
- if (!from || !to || !local(from) || !local(to)) {
367
+ if (!from || !to || (!local(from) && from.kind !== 'route') || (!local(to) && to.kind !== 'route')) {
198
368
  globalGaps.push({ target: `${from ? label(from) : edge.source} ${edge.kind} ${to ? label(to) : edge.target}`,
199
369
  reason: 'unavailable', nextAnchor: from ? location(from) : edge.source });
200
370
  continue;
@@ -267,6 +437,19 @@ function buildArktsEvidencePacks(graph, options) {
267
437
  globalGaps.push({ target: `connecting path in the bounded graph slice: ${label(relationFocus[0])} → ${label(disconnected[0])}`,
268
438
  reason: 'unindexed_connection', nextAnchor: location(disconnected[0]) });
269
439
  }
440
+ // Rank already-read, hash-verified declarations by joint targets in the same file.
441
+ // Never discard a dependent helper merely because it has no UI label.
442
+ if (inspector && targetChecks) {
443
+ const byFile = new Map();
444
+ for (const unit of sources.values()) {
445
+ const list = byFile.get(unit.node.filePath) ?? [];
446
+ list.push(requestUnit(unit));
447
+ byFile.set(unit.node.filePath, list);
448
+ }
449
+ const scores = new Map([...byFile].map(([file, units]) => [file, inspector.score(units)]));
450
+ for (const pack of packs)
451
+ pack.priority += 30 * Math.max(0, ...pack.sources.map(id => scores.get(sources.get(id).node.filePath) ?? 0));
452
+ }
270
453
  const selected = [];
271
454
  const rejectedGap = (p) => p.gaps.length ? p.gaps : [{ target: p.label, reason: 'budget',
272
455
  nextAnchor: sources.get(p.sources[0] ?? '') ? location(sources.get(p.sources[0]).node) : p.label }];
@@ -281,6 +464,12 @@ function buildArktsEvidencePacks(graph, options) {
281
464
  return units.filter(s => !units.some(other => other.id !== s.id && other.node.filePath === s.node.filePath
282
465
  && other.start <= s.start && other.end >= s.end)).sort((a, b) => a.node.filePath.localeCompare(b.node.filePath) || a.start - b.start);
283
466
  };
467
+ const controlCache = new Map();
468
+ const controlsFor = (chosen) => unitsFor(chosen).flatMap(unit => {
469
+ if (!controlCache.has(unit))
470
+ controlCache.set(unit, (0, request_evidence_1.inspectControlEvidence)([requestUnit(unit)]));
471
+ return controlCache.get(unit);
472
+ }).slice(0, 8);
284
473
  const relationsFor = (chosen) => [...new Map(chosen.flatMap(p => p.path?.relations ?? (p.relation ? [p.relation] : []))
285
474
  .map(r => [JSON.stringify(r), r])).values()];
286
475
  const stopFor = (chosen) => search?.stopReason === 'supported'
@@ -291,6 +480,16 @@ function buildArktsEvidencePacks(graph, options) {
291
480
  'Complete declarations preserve internal branches. Enclosing callers and runtime order/value flow are not proven. Retrieval is not task completion.'];
292
481
  if (search)
293
482
  lines.push(`Path goal: ${search.goal.kind}; search direction: ${search.goal.direction}; stop: ${stopFor(chosen)}. Static paths do not prove runtime behavior.`);
483
+ if (inspector)
484
+ lines.push(...(0, request_evidence_1.renderRequestEvidence)(inspector.inspect(unitsFor(chosen).map(requestUnit)), targetChecks, behaviorChecks));
485
+ if ((0, implementation_context_1.controlEvidenceEnabled)())
486
+ lines.push(...(0, request_evidence_1.renderControlEvidence)(controlsFor(chosen)));
487
+ for (const pack of chosen.filter(p => p.context)) {
488
+ lines.push(`**Implementation context: ${inline(pack.context.label)}**`, ...pack.context.notes.map(n => `- ${inline(n)}`));
489
+ const sdk = pack.context.sdk;
490
+ if (sdk)
491
+ lines.push(`Indexed SDK signatures for ${inline(sdk.module)}; database version ${inline(sdk.version)}. Not source bodies or device validation.`, ...sdk.nodes.map(n => `- ${inline(n.filePath)}:${n.startLine} ${inline(n.signature || n.name)}${n.docstring ? ` — ${inline(n.docstring).slice(0, 500)}${n.docstring.length > 500 ? ' [documentation excerpt; remaining constraints not shown]' : ''}` : ''}`));
492
+ }
294
493
  const chosenPaths = chosen.filter(p => p.path);
295
494
  if (chosenPaths.length)
296
495
  lines.push('**Directed paths (all source dependencies provided)**', ...chosenPaths.map(p => `- ${inline(p.label)}`));
@@ -304,7 +503,7 @@ function buildArktsEvidencePacks(graph, options) {
304
503
  for (const unit of unitsFor(chosen)) {
305
504
  // Fence longer than any source backtick run: source-like guidance cannot escape.
306
505
  const fence = '`'.repeat(Math.max(3, ...Array.from(unit.source.matchAll(/`+/g), m => m[0].length + 1)));
307
- lines.push(`### ${inline(unit.node.filePath)}:${unit.start}-${unit.end} — complete declaration; file-fingerprint=${unit.fingerprint}`, fence + unit.node.language, unit.source.split('\n').map((line, i) => `${unit.start + i}\t${line}`).join('\n'), fence);
506
+ lines.push(`### ${inline(unit.node.filePath)}:${unit.start}-${unit.end} — ${unit.snapshot ? 'current configuration snapshot' : unit.node.kind === 'file' ? 'complete indexed file' : 'complete declaration'}; file-fingerprint=${unit.fingerprint}`, fence + unit.node.language, unit.source.split('\n').map((line, i) => `${unit.start + i}\t${line}`).join('\n'), fence);
308
507
  }
309
508
  if (gaps.length) {
310
509
  lines.push('**Gaps — inspect only the missing evidence needed for the task**', ...gaps.slice(0, 6).map(g => `- ${g.reason}: ${inline(g.target)}. Next anchor: ${inline(g.nextAnchor)}.`));
@@ -341,11 +540,20 @@ function buildArktsEvidencePacks(graph, options) {
341
540
  const locatedNodes = candidates.filter(n => units.some(s => s.node.filePath === n.filePath && s.start <= n.startLine && s.end >= n.endLine))
342
541
  .map(n => ({ id: n.id, name: n.name, qualifiedName: n.qualifiedName, filePath: n.filePath, startLine: n.startLine }));
343
542
  const status = !units.length ? 'empty' : gaps.length ? 'partial' : 'complete';
543
+ const checkedRequest = inspector?.inspect(units.map(requestUnit));
544
+ const requestEvidence = checkedRequest ? { ...checkedRequest, targets: targetChecks ? checkedRequest.targets : [],
545
+ behaviors: behaviorChecks ? checkedRequest.behaviors : [] } : undefined;
344
546
  return { text, emission: { projectRoot: options.projectRoot, query: options.query, files,
345
547
  sourceBytes: files.reduce((sum, f) => sum + f.bytes, 0), responseBytes: text.length, locatedNodes,
346
548
  evidenceStatus: status, partial: status !== 'complete', coveredObligations: selected.map(p => p.label),
347
549
  uncoveredObligations: gaps.map(g => `${g.reason}: ${g.target}`), nextAnchor: gaps[0]?.nextAnchor },
348
- metadata: { version: search ? 2 : 1, scope: 'bounded_static_evidence', status, selectedPacks: selected.map(p => p.id), gaps,
550
+ metadata: { ...((0, implementation_context_1.controlEvidenceEnabled)() ? { controls: controlsFor(selected) } : {}),
551
+ ...(contextEnabled ? { implementationContext: selected.flatMap(p => p.context ? [{ ...p.context,
552
+ ...(p.context.sdk ? { sdk: { ...p.context.sdk, nodes: p.context.sdk.nodes.map(n => ({
553
+ id: n.id, name: n.name, qualifiedName: n.qualifiedName, kind: n.kind, filePath: n.filePath,
554
+ startLine: n.startLine, endLine: n.endLine, startColumn: n.startColumn, endColumn: n.endColumn,
555
+ language: n.language, updatedAt: n.updatedAt, signature: n.signature,
556
+ })) } } : {}) }] : []) } : {}), ...(requestEvidence ? { requestEvidence } : {}), version: search ? 2 : 1, scope: 'bounded_static_evidence', status, selectedPacks: selected.map(p => p.id), gaps,
349
557
  relations: relationsFor(selected), ...(search ? { pathSearch: { goal: search.goal, stopReason: stopFor(selected),
350
558
  limitsHit: search.limitsHit, stats: search.stats, paths: search.paths.map(p => ({ id: p.id, nodeIds: p.nodeIds, cost: p.cost,
351
559
  evidence: selected.some(c => c.path?.id === p.id) ? 'provided' : 'partial' })) } } : {}) } };
@@ -0,0 +1,3 @@
1
+ /** Opt-in local evidence only, never an answer cache. Logging cannot break retrieval. */
2
+ export declare function recordEvidencePack(projectRoot: string, input: unknown, output: unknown): 'written' | 'disabled' | 'limit' | 'unavailable';
3
+ //# sourceMappingURL=evidence-audit.d.ts.map