homegraph 1.5.2 → 1.5.3

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 (150) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +15 -8
  3. package/dist/arkui/index.d.ts +4 -0
  4. package/dist/arkui/index.d.ts.map +1 -0
  5. package/dist/arkui/index.js +23 -0
  6. package/dist/arkui/index.js.map +1 -0
  7. package/dist/arkui/migrate-passage.d.ts +28 -0
  8. package/dist/arkui/migrate-passage.d.ts.map +1 -0
  9. package/dist/arkui/migrate-passage.js +310 -0
  10. package/dist/arkui/migrate-passage.js.map +1 -0
  11. package/dist/arkui/migrate-semantics.d.ts +47 -0
  12. package/dist/arkui/migrate-semantics.d.ts.map +1 -0
  13. package/dist/arkui/migrate-semantics.js +229 -0
  14. package/dist/arkui/migrate-semantics.js.map +1 -0
  15. package/dist/arkui/migrate-snapshot.d.ts +79 -0
  16. package/dist/arkui/migrate-snapshot.d.ts.map +1 -0
  17. package/dist/arkui/migrate-snapshot.js +340 -0
  18. package/dist/arkui/migrate-snapshot.js.map +1 -0
  19. package/dist/bin/fatal-handler.js +2 -2
  20. package/dist/bin/homegraph.js +105 -156
  21. package/dist/bin/homegraph.js.map +1 -1
  22. package/dist/bin/node-version-check.d.ts +6 -15
  23. package/dist/bin/node-version-check.d.ts.map +1 -1
  24. package/dist/bin/node-version-check.js +8 -37
  25. package/dist/bin/node-version-check.js.map +1 -1
  26. package/dist/db/index.d.ts +36 -1
  27. package/dist/db/index.d.ts.map +1 -1
  28. package/dist/db/index.js +92 -6
  29. package/dist/db/index.js.map +1 -1
  30. package/dist/db/migrations.d.ts +1 -1
  31. package/dist/db/migrations.d.ts.map +1 -1
  32. package/dist/db/migrations.js +15 -1
  33. package/dist/db/migrations.js.map +1 -1
  34. package/dist/db/queries.d.ts +42 -0
  35. package/dist/db/queries.d.ts.map +1 -1
  36. package/dist/db/queries.js +177 -10
  37. package/dist/db/queries.js.map +1 -1
  38. package/dist/db/schema.sql +6 -1
  39. package/dist/db/sqlite-adapter.d.ts +8 -3
  40. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  41. package/dist/db/sqlite-adapter.js +45 -3
  42. package/dist/db/sqlite-adapter.js.map +1 -1
  43. package/dist/extraction/extraction-version.d.ts +1 -1
  44. package/dist/extraction/extraction-version.js +1 -1
  45. package/dist/extraction/generated-detection.d.ts +42 -11
  46. package/dist/extraction/generated-detection.d.ts.map +1 -1
  47. package/dist/extraction/generated-detection.js +141 -20
  48. package/dist/extraction/generated-detection.js.map +1 -1
  49. package/dist/extraction/index.d.ts +42 -0
  50. package/dist/extraction/index.d.ts.map +1 -1
  51. package/dist/extraction/index.js +96 -0
  52. package/dist/extraction/index.js.map +1 -1
  53. package/dist/extraction/languages/arkts.d.ts +19 -2
  54. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  55. package/dist/extraction/languages/arkts.js +460 -110
  56. package/dist/extraction/languages/arkts.js.map +1 -1
  57. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  58. package/dist/extraction/wasm-runtime-flags.js +9 -6
  59. package/dist/extraction/wasm-runtime-flags.js.map +1 -1
  60. package/dist/graph-sources.d.ts +41 -0
  61. package/dist/graph-sources.d.ts.map +1 -0
  62. package/dist/graph-sources.js +82 -0
  63. package/dist/graph-sources.js.map +1 -0
  64. package/dist/index.d.ts +19 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +79 -9
  67. package/dist/index.js.map +1 -1
  68. package/dist/installer/index.d.ts.map +1 -1
  69. package/dist/installer/index.js +0 -48
  70. package/dist/installer/index.js.map +1 -1
  71. package/dist/installer/targets/antigravity.js +1 -1
  72. package/dist/installer/targets/antigravity.js.map +1 -1
  73. package/dist/installer/targets/deveco.js +1 -1
  74. package/dist/installer/targets/deveco.js.map +1 -1
  75. package/dist/installer/targets/hermes.d.ts +1 -1
  76. package/dist/installer/targets/hermes.js +2 -2
  77. package/dist/installer/targets/hermes.js.map +1 -1
  78. package/dist/installer/targets/opencode.js +1 -1
  79. package/dist/installer/targets/opencode.js.map +1 -1
  80. package/dist/installer/targets/shared.js +1 -1
  81. package/dist/installer/targets/shared.js.map +1 -1
  82. package/dist/mcp/daemon-paths.d.ts +5 -4
  83. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  84. package/dist/mcp/daemon-paths.js +28 -13
  85. package/dist/mcp/daemon-paths.js.map +1 -1
  86. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  87. package/dist/mcp/daemon-registry.js +4 -1
  88. package/dist/mcp/daemon-registry.js.map +1 -1
  89. package/dist/mcp/daemon.d.ts +1 -1
  90. package/dist/mcp/daemon.js +1 -1
  91. package/dist/mcp/engine.d.ts.map +1 -1
  92. package/dist/mcp/engine.js +12 -3
  93. package/dist/mcp/engine.js.map +1 -1
  94. package/dist/mcp/explore-dedup.d.ts +137 -0
  95. package/dist/mcp/explore-dedup.d.ts.map +1 -0
  96. package/dist/mcp/explore-dedup.js +236 -0
  97. package/dist/mcp/explore-dedup.js.map +1 -0
  98. package/dist/mcp/explore-session-state.d.ts +217 -0
  99. package/dist/mcp/explore-session-state.d.ts.map +1 -0
  100. package/dist/mcp/explore-session-state.js +322 -0
  101. package/dist/mcp/explore-session-state.js.map +1 -0
  102. package/dist/mcp/index.d.ts.map +1 -1
  103. package/dist/mcp/index.js +4 -7
  104. package/dist/mcp/index.js.map +1 -1
  105. package/dist/mcp/proxy.d.ts.map +1 -1
  106. package/dist/mcp/proxy.js +5 -14
  107. package/dist/mcp/proxy.js.map +1 -1
  108. package/dist/mcp/query-worker.d.ts +1 -1
  109. package/dist/mcp/query-worker.js +1 -1
  110. package/dist/mcp/server-instructions.d.ts +7 -2
  111. package/dist/mcp/server-instructions.d.ts.map +1 -1
  112. package/dist/mcp/server-instructions.js +104 -28
  113. package/dist/mcp/server-instructions.js.map +1 -1
  114. package/dist/mcp/session.d.ts +14 -3
  115. package/dist/mcp/session.d.ts.map +1 -1
  116. package/dist/mcp/session.js +17 -14
  117. package/dist/mcp/session.js.map +1 -1
  118. package/dist/mcp/startup-handshake.d.ts +1 -1
  119. package/dist/mcp/startup-handshake.js +1 -1
  120. package/dist/mcp/tools.d.ts +117 -7
  121. package/dist/mcp/tools.d.ts.map +1 -1
  122. package/dist/mcp/tools.js +3056 -503
  123. package/dist/mcp/tools.js.map +1 -1
  124. package/dist/resolution/frameworks/arkts-napi.d.ts +14 -0
  125. package/dist/resolution/frameworks/arkts-napi.d.ts.map +1 -1
  126. package/dist/resolution/frameworks/arkts-napi.js +169 -49
  127. package/dist/resolution/frameworks/arkts-napi.js.map +1 -1
  128. package/dist/search/query-utils.d.ts +116 -4
  129. package/dist/search/query-utils.d.ts.map +1 -1
  130. package/dist/search/query-utils.js +869 -60
  131. package/dist/search/query-utils.js.map +1 -1
  132. package/dist/types.d.ts +8 -0
  133. package/dist/types.d.ts.map +1 -1
  134. package/dist/upgrade/index.d.ts +14 -26
  135. package/dist/upgrade/index.d.ts.map +1 -1
  136. package/dist/upgrade/index.js +28 -123
  137. package/dist/upgrade/index.js.map +1 -1
  138. package/dist/upgrade/remove-binary.d.ts +19 -19
  139. package/dist/upgrade/remove-binary.d.ts.map +1 -1
  140. package/dist/upgrade/remove-binary.js +20 -21
  141. package/dist/upgrade/remove-binary.js.map +1 -1
  142. package/dist/upgrade/update-check.d.ts +4 -3
  143. package/dist/upgrade/update-check.d.ts.map +1 -1
  144. package/dist/upgrade/update-check.js +4 -3
  145. package/dist/upgrade/update-check.js.map +1 -1
  146. package/package.json +12 -8
  147. package/dist/telemetry/index.d.ts +0 -143
  148. package/dist/telemetry/index.d.ts.map +0 -1
  149. package/dist/telemetry/index.js +0 -541
  150. package/dist/telemetry/index.js.map +0 -1
@@ -80,6 +80,8 @@ exports.resetArkTSBatch = resetArkTSBatch;
80
80
  exports.shrinkArkTSBatchPostParse = shrinkArkTSBatchPostParse;
81
81
  exports.collectExportedRtaEntryPointsFromFiles = collectExportedRtaEntryPointsFromFiles;
82
82
  exports.stateDecoratorKinds = stateDecoratorKinds;
83
+ exports.fieldDecoratorEntries = fieldDecoratorEntries;
84
+ exports.fieldDecoratorsEncoded = fieldDecoratorsEncoded;
83
85
  exports.stateTransferViaForField = stateTransferViaForField;
84
86
  exports.isUnclearArkClassSignature = isUnclearArkClassSignature;
85
87
  exports.isUnclearArkMethodSignature = isUnclearArkMethodSignature;
@@ -105,6 +107,7 @@ exports.readOhosSdkPlatformVersion = readOhosSdkPlatformVersion;
105
107
  exports.discoverLocalOhosSdkCandidates = discoverLocalOhosSdkCandidates;
106
108
  exports.findLocalOhosSdkForVersion = findLocalOhosSdkForVersion;
107
109
  exports.resolveOhosApiVersionForProject = resolveOhosApiVersionForProject;
110
+ exports.tryDownloadOhosApiDbFromNpm = tryDownloadOhosApiDbFromNpm;
108
111
  exports.ensureOhosApiDb = ensureOhosApiDb;
109
112
  exports.attachExistingOhosApiDbForProject = attachExistingOhosApiDbForProject;
110
113
  exports.bindOhosApiDbForProject = bindOhosApiDbForProject;
@@ -122,9 +125,12 @@ const directory_1 = require("../../directory");
122
125
  const project_config_1 = require("../../project-config");
123
126
  const context_1 = require("../context");
124
127
  const default_ignore_1 = require("../default-ignore");
128
+ const generated_detection_1 = require("../generated-detection");
125
129
  const extraction_version_1 = require("../extraction-version");
126
130
  const version_1 = require("../../mcp/version");
127
131
  const liveness_watchdog_1 = require("../../mcp/liveness-watchdog");
132
+ const migrate_semantics_1 = require("../../arkui/migrate-semantics");
133
+ const migrate_passage_1 = require("../../arkui/migrate-passage");
128
134
  const tree_sitter_helpers_1 = require("../tree-sitter-helpers");
129
135
  const wasm_runtime_flags_1 = require("../wasm-runtime-flags");
130
136
  const ignore_1 = __importDefault(require("ignore"));
@@ -480,11 +486,16 @@ function hasHarmonyProjectMarkers(rootDir) {
480
486
  function normIndexPath(filePath) {
481
487
  return filePath.replace(/\\/g, '/');
482
488
  }
483
- /** Member calls through a native `.so` binding: `sdk.Asset_setCropRect(`. */
489
+ /** Photos-style member calls: `sdk.Asset_setCropRect(`. */
484
490
  const NAPI_MEMBER_CALL_RE = /\.\s*([A-Z][A-Za-z0-9_]*(?:_[A-Za-z0-9_]+)+)\s*\(/g;
491
+ /** `libFoo.so` module path from ArkAnalyzer import infos. */
492
+ const LIB_SO_MODULE_RE = /^lib[A-Za-z0-9_]+\.so$/;
485
493
  function isNapiSymbolName(name) {
486
494
  return /^[A-Z][A-Za-z0-9_]*_[A-Za-z0-9_]+$/.test(name);
487
495
  }
496
+ function escapeRegExp(s) {
497
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
498
+ }
488
499
  let persistedBatch = null;
489
500
  /** File that triggered the most recent batch persist (for indexAll edge stats). */
490
501
  let batchTriggerFile = null;
@@ -635,6 +646,7 @@ function persistFileResult(queries, filePath, content, stats, result, options) {
635
646
  indexedAt: Date.now(),
636
647
  nodeCount: result.nodes.length,
637
648
  errors: result.errors.length > 0 ? result.errors : undefined,
649
+ generated: (0, generated_detection_1.detectGeneratedFile)(filePath, content),
638
650
  };
639
651
  queries.upsertFile(fileRecord);
640
652
  }
@@ -888,7 +900,9 @@ function runArkTSBatchFullCore(rootDir, queries, triggerFile, etsFiles, batchKey
888
900
  const index = buildArkTSIndex(rootDir, etsFiles);
889
901
  if (index.fileResults.size === 0) {
890
902
  const fatal = index.errors.find((e) => e.severity === 'error');
891
- throw new Error(fatal?.message ?? 'ArkTS batch produced no indexed files');
903
+ const detail = index.errors.map((e) => `[${e.severity}] ${e.message}`).join(' | ');
904
+ throw new Error(fatal?.message ??
905
+ `ArkTS batch produced no indexed files${detail ? ` (${detail})` : ' (no Scene/adapter errors recorded)'}`);
892
906
  }
893
907
  queries.deleteArkTSCrossFileCallEdges();
894
908
  persistBatchResultsSync(rootDir, queries, index);
@@ -1025,7 +1039,9 @@ async function primeArkTSBatch(rootDir, queries, triggerFile, options) {
1025
1039
  const streamed = index.streamedPersistedPaths?.size ?? 0;
1026
1040
  if (index.fileResults.size === 0 && streamed === 0) {
1027
1041
  const fatal = index.errors.find((e) => e.severity === 'error');
1028
- throw new Error(fatal?.message ?? 'ArkTS batch produced no indexed files');
1042
+ const detail = index.errors.map((e) => `[${e.severity}] ${e.message}`).join(' | ');
1043
+ throw new Error(fatal?.message ??
1044
+ `ArkTS batch produced no indexed files${detail ? ` (${detail})` : ' (no Scene/adapter errors recorded)'}`);
1029
1045
  }
1030
1046
  if (!streamModular) {
1031
1047
  queries.deleteArkTSCrossFileCallEdges();
@@ -1198,8 +1214,33 @@ const FIELD_CATEGORY = {
1198
1214
  };
1199
1215
  /** {@link ModifierType.CONST} — not re-exported from the arkanalyzer npm entry. */
1200
1216
  const MODIFIER_CONST = 128;
1217
+ /**
1218
+ * Project-relative path for an ArkAnalyzer absolute file path.
1219
+ *
1220
+ * ArkAnalyzer returns realpath'd absolutes (macOS: `/private/var/...`), while
1221
+ * HomeGraph's `rootDir` from `mkdtemp` / callers is often the unresolved form
1222
+ * (`/var/...`). Without canonicalizing both sides, `path.relative` escapes the
1223
+ * project (`../../../../private/var/.../file.ets`) and every file is skipped
1224
+ * by the `scanned` set → empty batch / "no indexed files".
1225
+ */
1201
1226
  function normalizeRelPath(rootDir, absolutePath) {
1202
- return path.relative(rootDir, absolutePath).replace(/\\/g, '/');
1227
+ const root = realpathExisting(rootDir);
1228
+ const abs = realpathExisting(absolutePath);
1229
+ return path.relative(root, abs).replace(/\\/g, '/');
1230
+ }
1231
+ /** `realpathSync` when the path (or its dirname) exists; otherwise `path.resolve`. */
1232
+ function realpathExisting(p) {
1233
+ try {
1234
+ return fs.realpathSync(p);
1235
+ }
1236
+ catch {
1237
+ try {
1238
+ return path.join(fs.realpathSync(path.dirname(p)), path.basename(p));
1239
+ }
1240
+ catch {
1241
+ return path.resolve(p);
1242
+ }
1243
+ }
1203
1244
  }
1204
1245
  /** Map ArkAnalyzer synthetic files (e.g. @dummyFile) to our virtual indexed path. */
1205
1246
  function resolveArkanalyzerVirtualPath(arkFile) {
@@ -1655,6 +1696,52 @@ function stateDecoratorKinds(field) {
1655
1696
  return [];
1656
1697
  return [...decs].map((d) => d.getKind()).filter(Boolean);
1657
1698
  }
1699
+ /** All field decorators with optional string args (Provide('theme') → theme). */
1700
+ function fieldDecoratorEntries(field) {
1701
+ const out = [];
1702
+ const seen = new Set();
1703
+ const push = (kind, arg) => {
1704
+ if (!kind || seen.has(kind))
1705
+ return;
1706
+ seen.add(kind);
1707
+ out.push(arg ? { kind, arg } : { kind });
1708
+ };
1709
+ try {
1710
+ for (const d of field.getDecorators?.() ?? []) {
1711
+ push(d.getKind(), (0, migrate_semantics_1.parseDecoratorArgFromContent)(d.getContent?.() ?? ''));
1712
+ }
1713
+ }
1714
+ catch {
1715
+ /* ignore */
1716
+ }
1717
+ if (out.length === 0) {
1718
+ for (const kind of stateDecoratorKinds(field))
1719
+ push(kind);
1720
+ }
1721
+ return out;
1722
+ }
1723
+ /** Encoded decorators list for node.decorators (bare kinds + Kind@arg). */
1724
+ function fieldDecoratorsEncoded(field) {
1725
+ return (0, migrate_semantics_1.encodeDecoratorEntries)(fieldDecoratorEntries(field));
1726
+ }
1727
+ function modelDecoratorEntries(model) {
1728
+ const out = [];
1729
+ const seen = new Set();
1730
+ try {
1731
+ for (const d of model.getDecorators?.() ?? []) {
1732
+ const kind = d.getKind();
1733
+ if (!kind || seen.has(kind))
1734
+ continue;
1735
+ seen.add(kind);
1736
+ const arg = (0, migrate_semantics_1.parseDecoratorArgFromContent)(d.getContent?.() ?? '');
1737
+ out.push(arg ? { kind, arg } : { kind });
1738
+ }
1739
+ }
1740
+ catch {
1741
+ /* ignore */
1742
+ }
1743
+ return out;
1744
+ }
1658
1745
  /** Child @Prop / @Link — edge `via` uses the decorator name as-is. */
1659
1746
  function stateTransferViaForField(field) {
1660
1747
  for (const kind of stateDecoratorKinds(field)) {
@@ -2001,12 +2088,12 @@ function indexViewTreeForClass(ctx, cls, result, relativePath) {
2001
2088
  if (!buildId)
2002
2089
  return;
2003
2090
  const seen = new Set();
2004
- const link = (source, target, kind, via, line) => {
2091
+ const link = (source, target, kind, via, line, extraMeta) => {
2005
2092
  const key = `${source}>${target}>${kind}>${via}`;
2006
2093
  if (seen.has(key))
2007
2094
  return;
2008
2095
  seen.add(key);
2009
- ctx.addEdge(result, source, target, kind, relativePath, via, line);
2096
+ ctx.addEdge(result, source, target, kind, relativePath, via, line, extraMeta);
2010
2097
  };
2011
2098
  for (const [field] of viewTree.getStateValues()) {
2012
2099
  const fieldId = ctx.resolveFieldNodeId(field);
@@ -2074,32 +2161,62 @@ function indexViewTreeForClass(ctx, cls, result, relativePath) {
2074
2161
  // Skip this node's signature edge; keep walking siblings/children.
2075
2162
  }
2076
2163
  }
2077
- if (node.stateValuesTransfer) {
2078
- for (const [childField, parentValue] of node.stateValuesTransfer) {
2079
- const childFieldId = ctx.resolveFieldNodeId(childField);
2080
- if (parentValue instanceof arkanalyzer_1.ArkField) {
2081
- const via = stateTransferViaForField(childField);
2082
- if (!via || !childFieldId)
2083
- continue;
2084
- ctx.markFieldStateDecorators?.(childField, childFieldId, result);
2085
- const parentFieldId = ctx.resolveFieldNodeId(parentValue);
2086
- if (parentFieldId) {
2087
- ctx.markFieldStateDecorators?.(parentValue, parentFieldId, result);
2088
- link(parentFieldId, childFieldId, 'references', via, childField.getOriginFullPosition()?.getFirstLine() ?? 1);
2089
- }
2164
+ // Data passages for migrate snapshot (+ Prop/Link edges for explore).
2165
+ try {
2166
+ const passages = (0, migrate_passage_1.collectPassagesForViewTreeNode)(node, ctx.scene, cls);
2167
+ for (const p of passages) {
2168
+ const toId = ctx.resolveFieldNodeId(p.toField);
2169
+ if (!toId)
2170
+ continue;
2171
+ ctx.markFieldStateDecorators?.(p.toField, toId, result);
2172
+ let fromId = null;
2173
+ if (p.fromField) {
2174
+ fromId = ctx.resolveFieldNodeId(p.fromField);
2175
+ if (fromId)
2176
+ ctx.markFieldStateDecorators?.(p.fromField, fromId, result);
2090
2177
  }
2091
- else if (parentValue instanceof arkanalyzer_1.ArkMethod) {
2092
- let builderId = ctx.ensureMethodNode(parentValue, relativePath, result, classNodeId);
2093
- if (!builderId) {
2094
- try {
2095
- builderId = ctx.resolveMethodIdBySig(parentValue.getSignature());
2096
- }
2097
- catch {
2098
- builderId = null;
2178
+ if (!fromId && p.fromIsParentComponent)
2179
+ fromId = classNodeId;
2180
+ if (!fromId)
2181
+ continue;
2182
+ const line = p.toField.getOriginFullPosition()?.getFirstLine() ?? 1;
2183
+ link(fromId, toId, 'references', p.via || 'data-passage', line, {
2184
+ passageType: p.passageType,
2185
+ valueType: p.valueType,
2186
+ forcesMigration: p.forcesMigration,
2187
+ parentExpression: p.parentExpression,
2188
+ });
2189
+ }
2190
+ }
2191
+ catch {
2192
+ // Fall back to legacy Prop/Link-only transfer below.
2193
+ if (node.stateValuesTransfer) {
2194
+ for (const [childField, parentValue] of node.stateValuesTransfer) {
2195
+ const childFieldId = ctx.resolveFieldNodeId(childField);
2196
+ if (parentValue instanceof arkanalyzer_1.ArkField) {
2197
+ const via = stateTransferViaForField(childField);
2198
+ if (!via || !childFieldId)
2199
+ continue;
2200
+ ctx.markFieldStateDecorators?.(childField, childFieldId, result);
2201
+ const parentFieldId = ctx.resolveFieldNodeId(parentValue);
2202
+ if (parentFieldId) {
2203
+ ctx.markFieldStateDecorators?.(parentValue, parentFieldId, result);
2204
+ link(parentFieldId, childFieldId, 'references', via, childField.getOriginFullPosition()?.getFirstLine() ?? 1);
2099
2205
  }
2100
2206
  }
2101
- if (childFieldId && builderId) {
2102
- link(builderId, childFieldId, 'references', 'builder-param', childField.getOriginFullPosition()?.getFirstLine() ?? 1);
2207
+ else if (parentValue instanceof arkanalyzer_1.ArkMethod) {
2208
+ let builderId = ctx.ensureMethodNode(parentValue, relativePath, result, classNodeId);
2209
+ if (!builderId) {
2210
+ try {
2211
+ builderId = ctx.resolveMethodIdBySig(parentValue.getSignature());
2212
+ }
2213
+ catch {
2214
+ builderId = null;
2215
+ }
2216
+ }
2217
+ if (childFieldId && builderId) {
2218
+ link(builderId, childFieldId, 'references', 'builder-param', childField.getOriginFullPosition()?.getFirstLine() ?? 1);
2219
+ }
2103
2220
  }
2104
2221
  }
2105
2222
  }
@@ -2155,6 +2272,12 @@ class ArkTSAdapter {
2155
2272
  crossFileEdges = [];
2156
2273
  /** Per-module `@dummyMain@…` created during analyseByModule (for scene aggregator). */
2157
2274
  moduleDummyMains = [];
2275
+ /**
2276
+ * Local import binding names from `lib*.so` per relative file path.
2277
+ * Filled before method bodies are scanned so `multimodalinput.getTidByName(`
2278
+ * can emit NAPI call refs (not only photos-style `Class_method` names).
2279
+ */
2280
+ soImportBindingsByFile = new Map();
2158
2281
  constructor(rootDir, scannedFiles) {
2159
2282
  this.rootDir = rootDir;
2160
2283
  this.scanned = new Set(scannedFiles);
@@ -2688,6 +2811,70 @@ class ArkTSAdapter {
2688
2811
  }
2689
2812
  }
2690
2813
  }
2814
+ this.indexObservedClassRefs();
2815
+ }
2816
+ /**
2817
+ * State / Param fields whose type names an @Observed / @ObservedV2 class →
2818
+ * `references` via `observed-ref` (arkui-migrate).
2819
+ */
2820
+ indexObservedClassRefs() {
2821
+ const observedByName = new Map();
2822
+ for (const [, result] of this.fileResults) {
2823
+ for (const n of result.nodes) {
2824
+ if (n.kind !== 'class' && n.kind !== 'struct')
2825
+ continue;
2826
+ const decs = n.decorators ?? [];
2827
+ if (!decs.some((d) => migrate_semantics_1.OBSERVATION_DECORATORS.has(d.split('@')[0])))
2828
+ continue;
2829
+ const arr = observedByName.get(n.name) ?? [];
2830
+ arr.push(n.id);
2831
+ observedByName.set(n.name, arr);
2832
+ }
2833
+ }
2834
+ if (observedByName.size === 0)
2835
+ return;
2836
+ for (const [relativePath, result] of this.fileResults) {
2837
+ for (const field of result.nodes) {
2838
+ if (field.kind !== 'property' && field.kind !== 'field')
2839
+ continue;
2840
+ const typeStr = field.signature ?? '';
2841
+ const bare = typeStr.replace(/<.*>/, '').split(/[.\s|]/).filter(Boolean).pop();
2842
+ if (!bare)
2843
+ continue;
2844
+ const targets = observedByName.get(bare);
2845
+ if (!targets || targets.length === 0)
2846
+ continue;
2847
+ // Prefer unique name; if ambiguous, same-file first
2848
+ let targetId = targets[0];
2849
+ if (targets.length > 1) {
2850
+ const sameFile = targets.find((id) => {
2851
+ const n = result.nodes.find((x) => x.id === id);
2852
+ return n?.filePath === relativePath;
2853
+ });
2854
+ if (sameFile)
2855
+ targetId = sameFile;
2856
+ else
2857
+ continue; // ambiguous cross-file — skip (precision > recall)
2858
+ }
2859
+ const edgeKey = `${field.id}\0${targetId}\0observed-ref`;
2860
+ if (this.emittedEdgeKeys.has(edgeKey))
2861
+ continue;
2862
+ this.emittedEdgeKeys.add(edgeKey);
2863
+ const edge = arkEdge(field.id, targetId, 'references', {
2864
+ metadata: {
2865
+ synthesizedBy: 'arkui-migrate',
2866
+ via: 'observed-ref',
2867
+ registeredAt: `${relativePath}:${field.startLine}`,
2868
+ },
2869
+ });
2870
+ if (this.nodeInFile(targetId, relativePath)) {
2871
+ result.edges.push(edge);
2872
+ }
2873
+ else {
2874
+ this.crossFileEdges.push(edge);
2875
+ }
2876
+ }
2877
+ }
2691
2878
  }
2692
2879
  /** Index ArkAnalyzer's in-scene @dummyMain (DummyMainCreater output), not a HomeGraph synthetic node. */
2693
2880
  indexArkanalyzerDummyMain(dummyMain) {
@@ -2737,6 +2924,8 @@ class ArkTSAdapter {
2737
2924
  }
2738
2925
  const result = this.ensureFileResult(relativePath, lineCount);
2739
2926
  const fileId = `file:${relativePath}`;
2927
+ // Before methods: record `lib*.so` bindings so body NAPI call scan can see them.
2928
+ this.collectSoImportBindings(relativePath, arkFile);
2740
2929
  for (const ns of arkFile.getNamespaces()) {
2741
2930
  this.indexNamespace(relativePath, language, result, ns, fileId);
2742
2931
  }
@@ -2745,6 +2934,59 @@ class ArkTSAdapter {
2745
2934
  this.indexTypeAliases(relativePath, language, result, arkFile, undefined, fileId);
2746
2935
  this.indexModuleLocals(relativePath, language, result, arkFile, undefined, fileId);
2747
2936
  this.indexImports(relativePath, language, result, arkFile, fileId);
2937
+ this.indexStorageApiKeys(relativePath, arkFile, result);
2938
+ }
2939
+ /** AppStorage / LocalStorage / … literal keys → arkui-migrate edges from each component. */
2940
+ indexStorageApiKeys(relativePath, arkFile, result) {
2941
+ let code = '';
2942
+ try {
2943
+ code = arkFile.getCode() ?? '';
2944
+ }
2945
+ catch {
2946
+ return;
2947
+ }
2948
+ if (!code)
2949
+ return;
2950
+ const hits = (0, migrate_semantics_1.scanStorageApiKeys)(code);
2951
+ if (hits.length === 0)
2952
+ return;
2953
+ const componentIds = result.nodes
2954
+ .filter((n) => n.kind === 'component' && n.filePath === relativePath)
2955
+ .map((n) => n.id);
2956
+ if (componentIds.length === 0)
2957
+ return;
2958
+ for (const hit of hits) {
2959
+ const line = (0, migrate_semantics_1.lineOfOffset)(code, hit.index);
2960
+ for (const compId of componentIds) {
2961
+ const edgeKey = `${compId}\0storage-api\0${hit.channel}\0${hit.key}`;
2962
+ if (this.emittedEdgeKeys.has(edgeKey))
2963
+ continue;
2964
+ this.emittedEdgeKeys.add(edgeKey);
2965
+ result.edges.push(arkEdge(compId, compId, 'references', {
2966
+ metadata: {
2967
+ synthesizedBy: 'arkui-migrate',
2968
+ via: 'storage-api',
2969
+ channel: hit.channel,
2970
+ key: hit.key,
2971
+ method: hit.method,
2972
+ registeredAt: `${relativePath}:${line}`,
2973
+ },
2974
+ }));
2975
+ }
2976
+ }
2977
+ }
2978
+ collectSoImportBindings(relativePath, arkFile) {
2979
+ const set = new Set();
2980
+ for (const importInfo of arkFile.getImportInfos()) {
2981
+ const modulePath = importInfo.getFrom();
2982
+ if (!modulePath || !LIB_SO_MODULE_RE.test(modulePath))
2983
+ continue;
2984
+ const clause = importInfo.getImportClauseName();
2985
+ if (clause)
2986
+ set.add(clause);
2987
+ }
2988
+ if (set.size > 0)
2989
+ this.soImportBindingsByFile.set(relativePath, set);
2748
2990
  }
2749
2991
  indexModuleLocals(relativePath, language, result, arkFile, ns, parentId) {
2750
2992
  const declaringClass = ns ? ns.getDefaultClass() : arkFile.getDefaultClass();
@@ -2838,8 +3080,10 @@ class ArkTSAdapter {
2838
3080
  const qn = buildClassQualifiedName(cls);
2839
3081
  const kind = classNodeKind(cls);
2840
3082
  const { line, endLine, col } = linesFromPosition(cls.getOriginFullPosition());
3083
+ const classDecs = (0, migrate_semantics_1.encodeDecoratorEntries)(modelDecoratorEntries(cls));
2841
3084
  const classNode = makeNode(relativePath, language, kind, displayName, qn, line, endLine, col, {
2842
3085
  ...modelModifiersToNodeExtras(cls),
3086
+ ...(classDecs.length > 0 ? { decorators: classDecs } : {}),
2843
3087
  });
2844
3088
  this.addNode(result, classNode);
2845
3089
  this.classToId.set(cls, classNode.id);
@@ -2867,7 +3111,10 @@ class ArkTSAdapter {
2867
3111
  }
2868
3112
  }
2869
3113
  if (cls.hasComponentDecorator()) {
2870
- const componentNode = makeNode(relativePath, language, 'component', displayName, qn, line, endLine, col, { ...modelModifiersToNodeExtras(cls) });
3114
+ const componentNode = makeNode(relativePath, language, 'component', displayName, qn, line, endLine, col, {
3115
+ ...modelModifiersToNodeExtras(cls),
3116
+ ...(classDecs.length > 0 ? { decorators: classDecs } : {}),
3117
+ });
2871
3118
  this.addNode(result, componentNode);
2872
3119
  this.componentToId.set(cls, componentNode.id);
2873
3120
  try {
@@ -2953,10 +3200,10 @@ class ArkTSAdapter {
2953
3200
  return;
2954
3201
  const { line, endLine, col } = linesFromPosition(field.getOriginFullPosition());
2955
3202
  const qn = buildFieldQualifiedName(field);
2956
- const stateDecs = stateDecoratorKinds(field);
3203
+ const encodedDecs = fieldDecoratorsEncoded(field);
2957
3204
  const fieldNode = makeNode(relativePath, language, kind, name, qn, line, endLine, col, {
2958
3205
  signature: field.getType()?.toString(),
2959
- ...(stateDecs.length > 0 ? { decorators: stateDecs } : {}),
3206
+ ...(encodedDecs.length > 0 ? { decorators: encodedDecs } : {}),
2960
3207
  ...modelModifiersToNodeExtras(field),
2961
3208
  });
2962
3209
  this.addNode(result, fieldNode);
@@ -2982,19 +3229,19 @@ class ArkTSAdapter {
2982
3229
  classToId: this.classToId,
2983
3230
  fieldToId: this.fieldToId,
2984
3231
  nodeIds: this.nodeIds,
2985
- addEdge: (result, source, target, kind, callerFile, via, line) => this.addViewTreeEdge(result, source, target, kind, callerFile, via, line),
3232
+ addEdge: (result, source, target, kind, callerFile, via, line, extraMeta) => this.addViewTreeEdge(result, source, target, kind, callerFile, via, line, extraMeta),
2986
3233
  ensureMethodNode: (method, relativePath, result, parentId) => this.ensureMethodNode(method, relativePath, result, parentId),
2987
3234
  resolveClassNodeId: (cls) => this.resolveClassNodeId(cls),
2988
3235
  resolveClassIdBySig: (sig, importer) => this.resolveClassIdBySig(sig, importer),
2989
3236
  resolveMethodIdBySig: (sig, importer) => this.resolveMethodIdBySig(sig, importer),
2990
3237
  resolveFieldNodeId: (field) => this.resolveFieldNodeId(field),
2991
3238
  markFieldStateDecorators: (field, fieldNodeId, result) => {
2992
- const kinds = stateDecoratorKinds(field);
2993
- if (kinds.length === 0)
3239
+ const encoded = fieldDecoratorsEncoded(field);
3240
+ if (encoded.length === 0)
2994
3241
  return;
2995
3242
  const node = result.nodes.find((n) => n.id === fieldNodeId);
2996
3243
  if (node)
2997
- node.decorators = kinds;
3244
+ node.decorators = encoded;
2998
3245
  },
2999
3246
  };
3000
3247
  }
@@ -3218,7 +3465,7 @@ class ArkTSAdapter {
3218
3465
  this.indexMethod(relativePath, 'arkts', result, method, parentId, kind, displayName);
3219
3466
  return this.methodToId.get(method) ?? null;
3220
3467
  }
3221
- addViewTreeEdge(result, source, target, kind, callerFile, via, line) {
3468
+ addViewTreeEdge(result, source, target, kind, callerFile, via, line, extraMeta) {
3222
3469
  const edgeKey = `${source}\0${target}\0${kind}\0${via}`;
3223
3470
  if (this.emittedEdgeKeys.has(edgeKey))
3224
3471
  return;
@@ -3228,6 +3475,7 @@ class ArkTSAdapter {
3228
3475
  synthesizedBy: 'viewtree',
3229
3476
  via,
3230
3477
  registeredAt: `${callerFile}:${line}`,
3478
+ ...(extraMeta ?? {}),
3231
3479
  },
3232
3480
  });
3233
3481
  if (this.nodeInFile(target, callerFile)) {
@@ -3305,17 +3553,14 @@ class ArkTSAdapter {
3305
3553
  return;
3306
3554
  const baseLine = method.getImplOriginFullPosition()?.getFirstLine() ?? 1;
3307
3555
  const seenAtLine = new Set();
3308
- NAPI_MEMBER_CALL_RE.lastIndex = 0;
3309
- let m;
3310
- while ((m = NAPI_MEMBER_CALL_RE.exec(code)) !== null) {
3311
- const symbol = m[1];
3312
- if (!isNapiSymbolName(symbol))
3313
- continue;
3314
- const lineOffset = code.slice(0, m.index).split('\n').length - 1;
3556
+ const pushCall = (symbol, matchIndex) => {
3557
+ if (!symbol)
3558
+ return;
3559
+ const lineOffset = code.slice(0, matchIndex).split('\n').length - 1;
3315
3560
  const line = baseLine + lineOffset;
3316
3561
  const dedupKey = `${symbol}:${line}`;
3317
3562
  if (seenAtLine.has(dedupKey))
3318
- continue;
3563
+ return;
3319
3564
  seenAtLine.add(dedupKey);
3320
3565
  result.unresolvedReferences.push({
3321
3566
  fromNodeId,
@@ -3326,6 +3571,35 @@ class ArkTSAdapter {
3326
3571
  filePath: relativePath,
3327
3572
  language,
3328
3573
  });
3574
+ };
3575
+ // Photos-style `Class_method` on any receiver.
3576
+ NAPI_MEMBER_CALL_RE.lastIndex = 0;
3577
+ let m;
3578
+ while ((m = NAPI_MEMBER_CALL_RE.exec(code)) !== null) {
3579
+ const symbol = m[1];
3580
+ if (!isNapiSymbolName(symbol))
3581
+ continue;
3582
+ pushCall(symbol, m.index);
3583
+ }
3584
+ // `import x from 'libFoo.so'` → `x.draw(` / `x.getTidByName(` (camelCase OK).
3585
+ const soBindings = this.soImportBindingsByFile.get(relativePath);
3586
+ if (soBindings) {
3587
+ for (const binding of soBindings) {
3588
+ const re = new RegExp(`\\b${escapeRegExp(binding)}\\s*\\.\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`, 'g');
3589
+ while ((m = re.exec(code)) !== null) {
3590
+ pushCall(m[1], m.index);
3591
+ }
3592
+ }
3593
+ // define_class / instance path: `this.inst?.addVerticalRuler(` — receiver is
3594
+ // not the import binding. Only in files that already import a lib*.so.
3595
+ const anyMemberRe = /\?\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(|\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
3596
+ while ((m = anyMemberRe.exec(code)) !== null) {
3597
+ const symbol = m[1] ?? m[2];
3598
+ if (!symbol)
3599
+ continue;
3600
+ // Skip photos-style names already handled above; keep camelCase / plain ids.
3601
+ pushCall(symbol, m.index);
3602
+ }
3329
3603
  }
3330
3604
  }
3331
3605
  indexImports(relativePath, language, result, arkFile, fileId) {
@@ -4237,13 +4511,81 @@ function attachOhosApiDbBinding(queries, binding) {
4237
4511
  };
4238
4512
  }
4239
4513
  }
4514
+ /** Last-resort prebuilt npm package when the project's API version cannot be obtained. */
4515
+ const OHOS_API_FALLBACK_VERSION = '6.1.0';
4516
+ function runNpmPack(args, cwd) {
4517
+ const inv = process.platform === 'win32'
4518
+ ? { cmd: 'cmd.exe', args: ['/d', '/s', '/c', ['npm', ...args].join(' ')] }
4519
+ : { cmd: 'npm', args };
4520
+ (0, child_process_1.execFileSync)(inv.cmd, inv.args, {
4521
+ cwd,
4522
+ stdio: 'pipe',
4523
+ timeout: 180_000,
4524
+ windowsHide: true,
4525
+ env: process.env,
4526
+ });
4527
+ }
4240
4528
  /**
4241
- * Build (or reuse) ~/.homegraph/api/ohos-api-<version>.db from a local SDK.
4242
- * Never throws missing SDK / build failure returns a warning.
4529
+ * Fetch `homegraph-ohos-api-db-<version>` from npm and copy the db into
4530
+ * `~/.homegraph/api/`. Returns null when downloads are disabled, the package
4531
+ * is missing, or install fails. Opt out: HOMEGRAPH_OHOS_API_NO_DOWNLOAD=1.
4532
+ */
4533
+ function tryDownloadOhosApiDbFromNpm(version) {
4534
+ if (process.env.HOMEGRAPH_OHOS_API_NO_DOWNLOAD === '1')
4535
+ return null;
4536
+ const dbPath = ohosApiDbInstallPath(version);
4537
+ const packageName = ohosApiDbPackageName(version);
4538
+ if (fs.existsSync(dbPath)) {
4539
+ return { version, dbPath, packageName, installed: false };
4540
+ }
4541
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'hg-ohos-api-npm-'));
4542
+ try {
4543
+ runNpmPack(['pack', packageName, '--silent'], tmp);
4544
+ const tgz = fs.readdirSync(tmp).find((f) => f.endsWith('.tgz'));
4545
+ if (!tgz)
4546
+ return null;
4547
+ const extractDir = path.join(tmp, 'extract');
4548
+ fs.mkdirSync(extractDir, { recursive: true });
4549
+ (0, child_process_1.execFileSync)('tar', ['-xzf', path.join(tmp, tgz), '-C', extractDir], {
4550
+ stdio: 'pipe',
4551
+ timeout: 60_000,
4552
+ windowsHide: true,
4553
+ });
4554
+ const dbName = ohosApiDbFilename(version);
4555
+ const src = [
4556
+ path.join(extractDir, 'package', dbName),
4557
+ path.join(extractDir, dbName),
4558
+ ].find((p) => fs.existsSync(p)) ?? null;
4559
+ if (!src)
4560
+ return null;
4561
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
4562
+ fs.copyFileSync(src, dbPath);
4563
+ return { version, dbPath, packageName, installed: true };
4564
+ }
4565
+ catch {
4566
+ return null;
4567
+ }
4568
+ finally {
4569
+ try {
4570
+ fs.rmSync(tmp, { recursive: true, force: true });
4571
+ }
4572
+ catch {
4573
+ /* ignore */
4574
+ }
4575
+ }
4576
+ }
4577
+ /**
4578
+ * Build (or reuse) ~/.homegraph/api/ohos-api-<version>.db from a local SDK,
4579
+ * then npm prebuilts. Never throws — missing SDK / build / download failure
4580
+ * returns a warning.
4581
+ *
4582
+ * Order: existing db → local SDK build → npm pack for that version →
4583
+ * npm pack homegraph-ohos-api-db-6.1.0 fallback.
4243
4584
  */
4244
4585
  async function ensureOhosApiDb(version, options = {}) {
4245
4586
  const dbPath = ohosApiDbInstallPath(version);
4246
4587
  const packageName = ohosApiDbPackageName(version);
4588
+ // 1. Local db for the requested version
4247
4589
  if (fs.existsSync(dbPath)) {
4248
4590
  return { version, dbPath, packageName, installed: false };
4249
4591
  }
@@ -4255,6 +4597,7 @@ async function ensureOhosApiDb(version, options = {}) {
4255
4597
  'Explore will use project code only.',
4256
4598
  };
4257
4599
  }
4600
+ // 2. Build from a local DevEco / OpenHarmony SDK
4258
4601
  let sdkHome = null;
4259
4602
  if (options.sdkHomeHint) {
4260
4603
  const hintHomes = expandLocalOhosSdkHomes(options.sdkHomeHint);
@@ -4265,80 +4608,87 @@ async function ensureOhosApiDb(version, options = {}) {
4265
4608
  if (!sdkHome) {
4266
4609
  sdkHome = findLocalOhosSdkForVersion(version)?.sdkHome ?? null;
4267
4610
  }
4268
- if (!sdkHome) {
4269
- return {
4270
- code: 'ohos_api_sdk_missing',
4271
- message: `Local OHOS SDK for API ${version} not found (set OHOS_SDK_HOME / DEVECO_SDK_HOME / HOMEGRAPH_OHOS_SDK, ` +
4272
- 'or install the matching DevEco SDK). SDK API lookup disabled; project indexing continues normally.',
4273
- };
4274
- }
4275
- fs.mkdirSync(path.dirname(dbPath), { recursive: true });
4276
- options.onProgress?.({
4277
- phase: 'ohos-api',
4278
- current: 0,
4279
- total: 1,
4280
- currentFile: version,
4281
- });
4282
- try {
4283
- const result = await indexOhosApiDb({
4284
- sdkHome,
4285
- version,
4286
- outputPath: dbPath,
4287
- onProgress: options.onProgress
4288
- ? (progress) => {
4289
- options.onProgress?.({
4290
- phase: 'ohos-api',
4291
- current: progress.current,
4292
- total: progress.total || 1,
4293
- currentFile: progress.currentFile
4294
- ? `${version} ${path.basename(progress.currentFile)}`
4295
- : version,
4296
- subphase: progress.subphase,
4297
- });
4298
- }
4299
- : undefined,
4300
- });
4301
- if (!result.success || !fs.existsSync(dbPath)) {
4302
- try {
4303
- fs.unlinkSync(dbPath);
4304
- }
4305
- catch {
4306
- /* ignore */
4307
- }
4308
- const detail = result.errors
4309
- .filter((e) => e.severity === 'error')
4310
- .map((e) => e.message)
4311
- .join('; ');
4312
- return {
4313
- code: 'ohos_api_build_failed',
4314
- message: `Failed to build OHOS API db for ${version} from ${sdkHome}` +
4315
- (detail ? ` (${detail})` : '') +
4316
- '. SDK API lookup disabled; project indexing continues normally.',
4317
- };
4318
- }
4611
+ if (sdkHome) {
4612
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
4319
4613
  options.onProgress?.({
4320
4614
  phase: 'ohos-api',
4321
- current: 1,
4615
+ current: 0,
4322
4616
  total: 1,
4323
4617
  currentFile: version,
4324
4618
  });
4325
- return { version, dbPath, packageName, installed: true };
4326
- }
4327
- catch (err) {
4328
4619
  try {
4329
- if (fs.existsSync(dbPath))
4620
+ const result = await indexOhosApiDb({
4621
+ sdkHome,
4622
+ version,
4623
+ outputPath: dbPath,
4624
+ onProgress: options.onProgress
4625
+ ? (progress) => {
4626
+ options.onProgress?.({
4627
+ phase: 'ohos-api',
4628
+ current: progress.current,
4629
+ total: progress.total || 1,
4630
+ currentFile: progress.currentFile
4631
+ ? `${version} ${path.basename(progress.currentFile)}`
4632
+ : version,
4633
+ subphase: progress.subphase,
4634
+ });
4635
+ }
4636
+ : undefined,
4637
+ });
4638
+ if (result.success && fs.existsSync(dbPath)) {
4639
+ options.onProgress?.({
4640
+ phase: 'ohos-api',
4641
+ current: 1,
4642
+ total: 1,
4643
+ currentFile: version,
4644
+ });
4645
+ return { version, dbPath, packageName, installed: true };
4646
+ }
4647
+ try {
4330
4648
  fs.unlinkSync(dbPath);
4649
+ }
4650
+ catch {
4651
+ /* ignore */
4652
+ }
4331
4653
  }
4332
4654
  catch {
4333
- /* ignore */
4655
+ try {
4656
+ if (fs.existsSync(dbPath))
4657
+ fs.unlinkSync(dbPath);
4658
+ }
4659
+ catch {
4660
+ /* ignore */
4661
+ }
4334
4662
  }
4335
- const msg = err instanceof Error ? err.message : String(err);
4336
- return {
4337
- code: 'ohos_api_build_failed',
4338
- message: `Failed to build OHOS API db for ${version} from ${sdkHome} (${msg}). ` +
4339
- 'SDK API lookup disabled; project indexing continues normally.',
4340
- };
4341
4663
  }
4664
+ // 3. Download prebuilt for the requested version from npm
4665
+ const fromNpm = tryDownloadOhosApiDbFromNpm(version);
4666
+ if (fromNpm)
4667
+ return fromNpm;
4668
+ // 4. Last resort: homegraph-ohos-api-db-6.1.0
4669
+ if (version !== OHOS_API_FALLBACK_VERSION) {
4670
+ const fallbackPath = ohosApiDbInstallPath(OHOS_API_FALLBACK_VERSION);
4671
+ if (fs.existsSync(fallbackPath)) {
4672
+ return {
4673
+ version: OHOS_API_FALLBACK_VERSION,
4674
+ dbPath: fallbackPath,
4675
+ packageName: ohosApiDbPackageName(OHOS_API_FALLBACK_VERSION),
4676
+ installed: false,
4677
+ };
4678
+ }
4679
+ const fallback = tryDownloadOhosApiDbFromNpm(OHOS_API_FALLBACK_VERSION);
4680
+ if (fallback)
4681
+ return fallback;
4682
+ }
4683
+ return {
4684
+ code: 'ohos_api_sdk_missing',
4685
+ message: `OHOS API db for ${version} unavailable (no local SDK build, npm ${packageName} failed` +
4686
+ (version !== OHOS_API_FALLBACK_VERSION
4687
+ ? `, and fallback ${ohosApiDbPackageName(OHOS_API_FALLBACK_VERSION)} failed`
4688
+ : '') +
4689
+ '). Set OHOS_SDK_HOME / DEVECO_SDK_HOME or install the matching DevEco SDK. ' +
4690
+ 'SDK API lookup disabled; project indexing continues normally.',
4691
+ };
4342
4692
  }
4343
4693
  /**
4344
4694
  * Attach a prebuilt API db if present — never builds. Used on open / wireLayers.