homegraph 1.5.10 → 1.6.0

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 (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/extraction/grammars.d.ts +19 -0
  3. package/dist/extraction/grammars.js +44 -1
  4. package/dist/mcp/arkts-evidence-packs.js +1 -0
  5. package/dist/mcp/daemon.d.ts +21 -3
  6. package/dist/mcp/daemon.js +60 -6
  7. package/dist/mcp/engine.d.ts +26 -0
  8. package/dist/mcp/engine.js +143 -7
  9. package/dist/mcp/index-availability.d.ts +22 -0
  10. package/dist/mcp/index-availability.js +40 -1
  11. package/dist/mcp/index.js +9 -0
  12. package/dist/mcp/indexable-root.d.ts +22 -0
  13. package/dist/mcp/indexable-root.js +140 -0
  14. package/dist/mcp/liveness-watchdog.d.ts +6 -1
  15. package/dist/mcp/liveness-watchdog.js +17 -5
  16. package/dist/mcp/locate-contract.d.ts +50 -0
  17. package/dist/mcp/locate-contract.js +146 -0
  18. package/dist/mcp/server-instructions.d.ts +2 -2
  19. package/dist/mcp/server-instructions.js +7 -4
  20. package/dist/mcp/session.js +15 -0
  21. package/dist/mcp/tools.d.ts +49 -2
  22. package/dist/mcp/tools.js +714 -64
  23. package/dist/project-map/index.d.ts +45 -0
  24. package/dist/project-map/index.js +371 -3
  25. package/dist/resolution/callback-synthesizer.js +251 -0
  26. package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
  27. package/dist/resolution/frameworks/arkts-entry.js +513 -30
  28. package/dist/resolution/index.js +25 -21
  29. package/dist/runtime-log.d.ts +52 -0
  30. package/dist/runtime-log.js +199 -0
  31. package/dist/search/query-plan-provider.js +3 -2
  32. package/dist/search/query-plan.js +21 -19
  33. package/dist/search/query-utils.d.ts +13 -0
  34. package/dist/search/query-utils.js +64 -0
  35. package/package.json +1 -1
@@ -1,14 +1,63 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.arktsEntryResolver = void 0;
4
+ exports.harmonyModuleRootFromProfile = harmonyModuleRootFromProfile;
5
+ exports.resolveHarmonyPageSourcePath = resolveHarmonyPageSourcePath;
6
+ exports.parseHarmonyRouterMap = parseHarmonyRouterMap;
7
+ exports.parseHarmonyMainPages = parseHarmonyMainPages;
8
+ exports.parseHarmonyStringResources = parseHarmonyStringResources;
9
+ exports.parseHarmonyFormConfig = parseHarmonyFormConfig;
10
+ exports.parseHarmonyShortcutsConfig = parseHarmonyShortcutsConfig;
4
11
  exports.findArktsEntryPageComponent = findArktsEntryPageComponent;
5
12
  exports.findArktsLifecycleMethod = findArktsLifecycleMethod;
13
+ /**
14
+ * ArkTS entry / module manifest + Harmony route profile resolver (Spec 0039)
15
+ * + element/string.json lightweight constants (Spec 0041, no edges)
16
+ * + form_config / shortcuts_config capability profiles (Spec 0048, no UI edges).
17
+ *
18
+ * Parses:
19
+ * - `module.json5` pages → @Entry components, loadContent(url) → lifecycle
20
+ * - `route_map.json` / `router_map.json` routerMap[] → page / buildFunction
21
+ * - `main_pages.json` src[] → @Entry (same as module pages)
22
+ * - resources/.../element/string.json → constant nodes for FTS (no references)
23
+ * - form_config.json / shortcuts_config.json → capability constants (Spec 0048)
24
+ * - module.json5 form extensionAbilities + shortcuts/form metadata (Spec 0048)
25
+ */
26
+ const jsonc_parser_1 = require("jsonc-parser");
27
+ const grammars_1 = require("../../extraction/grammars");
6
28
  const LOAD_CONTENT_PAGE_RE = /loadContent\s*\(\s*['"]([^'"]+)['"]/g;
29
+ /** Cap string.json entries per file (Spec 0041). */
30
+ const STRING_JSON_ENTRY_CAP = 2000;
31
+ const STRING_JSON_VALUE_CAP = 200;
32
+ const CAPABILITY_ENTRY_CAP = 64;
7
33
  function pageStem(pagePath) {
8
34
  const normalized = pagePath.replace(/\\/g, '/');
9
35
  const base = normalized.split('/').pop() ?? normalized;
10
36
  return base.replace(/\.ets$/i, '');
11
37
  }
38
+ function lineOfNeedle(content, needle) {
39
+ const idx = content.indexOf(needle);
40
+ if (idx < 0)
41
+ return 1;
42
+ return content.slice(0, idx).split('\n').length;
43
+ }
44
+ /** Module root = path prefix before `/src/main/` (Harmony HAP/HAR layout). */
45
+ function harmonyModuleRootFromProfile(profileRel) {
46
+ const n = profileRel.replace(/\\/g, '/');
47
+ const marker = '/src/main/';
48
+ const i = n.indexOf(marker);
49
+ if (i < 0)
50
+ return '';
51
+ return n.slice(0, i);
52
+ }
53
+ /** Resolve `pageSourceFile` (module-relative) against the profile's module root. */
54
+ function resolveHarmonyPageSourcePath(profileRel, pageSourceFile) {
55
+ const modRoot = harmonyModuleRootFromProfile(profileRel);
56
+ const page = pageSourceFile.replace(/\\/g, '/').replace(/^\.\//, '');
57
+ if (!modRoot)
58
+ return page;
59
+ return `${modRoot}/${page}`.replace(/\/+/g, '/');
60
+ }
12
61
  function parseModuleJson5Pages(content) {
13
62
  const pages = [];
14
63
  const seen = new Set();
@@ -37,6 +86,63 @@ function parseModuleAbilities(content) {
37
86
  }
38
87
  return abilities;
39
88
  }
89
+ /** Parse standard Harmony `routerMap` JSON (route_map / router_map). */
90
+ function parseHarmonyRouterMap(content) {
91
+ let data;
92
+ try {
93
+ data = JSON.parse(content);
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ if (!data || typeof data !== 'object')
99
+ return [];
100
+ const arr = data.routerMap;
101
+ if (!Array.isArray(arr))
102
+ return [];
103
+ const out = [];
104
+ for (const item of arr) {
105
+ if (!item || typeof item !== 'object')
106
+ continue;
107
+ const rec = item;
108
+ const name = typeof rec.name === 'string' ? rec.name.trim() : '';
109
+ const pageSourceFile = typeof rec.pageSourceFile === 'string' ? rec.pageSourceFile.trim() : '';
110
+ if (!name || !pageSourceFile)
111
+ continue;
112
+ const buildFunction = typeof rec.buildFunction === 'string' && rec.buildFunction.trim()
113
+ ? rec.buildFunction.trim()
114
+ : undefined;
115
+ out.push({ name, pageSourceFile, buildFunction });
116
+ }
117
+ return out;
118
+ }
119
+ /** Parse `main_pages.json` `{ "src": ["pages/Index", ...] }`. */
120
+ function parseHarmonyMainPages(content) {
121
+ let data;
122
+ try {
123
+ data = JSON.parse(content);
124
+ }
125
+ catch {
126
+ return [];
127
+ }
128
+ if (!data || typeof data !== 'object')
129
+ return [];
130
+ const src = data.src;
131
+ if (!Array.isArray(src))
132
+ return [];
133
+ const out = [];
134
+ const seen = new Set();
135
+ for (const p of src) {
136
+ if (typeof p !== 'string')
137
+ continue;
138
+ const t = p.trim();
139
+ if (!t || t.includes('$') || seen.has(t))
140
+ continue;
141
+ seen.add(t);
142
+ out.push(t);
143
+ }
144
+ return out;
145
+ }
40
146
  function findArktsComponentByName(context, name) {
41
147
  for (const kind of ['component', 'struct', 'class']) {
42
148
  const iter = context.iterateNodesByKind?.(kind) ?? context.getNodesByKind(kind);
@@ -67,6 +173,33 @@ function findArktsPageComponent(context, pagePath) {
67
173
  }
68
174
  return null;
69
175
  }
176
+ function findArktsSymbolInFile(context, fileRel, name) {
177
+ const norm = fileRel.replace(/\\/g, '/');
178
+ const nodes = context.getNodesInFile(norm);
179
+ for (const n of nodes) {
180
+ if (n.name !== name)
181
+ continue;
182
+ if (n.kind === 'component' ||
183
+ n.kind === 'struct' ||
184
+ n.kind === 'class' ||
185
+ n.kind === 'function' ||
186
+ n.kind === 'method') {
187
+ return n;
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+ function findArktsBuilderByName(context, name) {
193
+ for (const kind of ['function', 'method']) {
194
+ const iter = context.iterateNodesByKind?.(kind) ?? context.getNodesByKind(kind);
195
+ for (const n of iter) {
196
+ if (n.language !== 'arkts' || n.name !== name)
197
+ continue;
198
+ return n;
199
+ }
200
+ }
201
+ return null;
202
+ }
70
203
  function findArktsMethodInComponent(context, component, methodName) {
71
204
  let owner = component;
72
205
  if (component.kind === 'component') {
@@ -86,12 +219,341 @@ function findArktsMethodInComponent(context, component, methodName) {
86
219
  }
87
220
  return null;
88
221
  }
222
+ function pushPageRoute(nodes, references, filePath, content, page, now) {
223
+ const line = lineOfNeedle(content, page);
224
+ const routeId = `arkts-route:${filePath}:${page}`;
225
+ nodes.push({
226
+ id: routeId,
227
+ kind: 'route',
228
+ name: page,
229
+ qualifiedName: `${filePath}::${page}`,
230
+ filePath,
231
+ language: 'yaml',
232
+ startLine: line,
233
+ endLine: line,
234
+ startColumn: 0,
235
+ endColumn: 0,
236
+ isExported: false,
237
+ updatedAt: now,
238
+ });
239
+ references.push({
240
+ fromNodeId: routeId,
241
+ referenceName: pageStem(page),
242
+ referenceKind: 'references',
243
+ line,
244
+ column: 0,
245
+ filePath,
246
+ language: 'yaml',
247
+ });
248
+ }
249
+ function extractRouterMapProfile(filePath, content, now) {
250
+ const nodes = [];
251
+ const references = [];
252
+ for (const entry of parseHarmonyRouterMap(content)) {
253
+ const line = lineOfNeedle(content, `"name": "${entry.name}"`) || lineOfNeedle(content, entry.name);
254
+ const pageRel = resolveHarmonyPageSourcePath(filePath, entry.pageSourceFile);
255
+ const routeId = `arkts-route:${filePath}:${entry.name}`;
256
+ const sigParts = [`pageSourceFile=${entry.pageSourceFile}`];
257
+ if (entry.buildFunction)
258
+ sigParts.push(`buildFunction=${entry.buildFunction}`);
259
+ nodes.push({
260
+ id: routeId,
261
+ kind: 'route',
262
+ name: entry.name,
263
+ qualifiedName: `${filePath}::${entry.name}`,
264
+ filePath,
265
+ language: 'yaml',
266
+ startLine: line,
267
+ endLine: line,
268
+ startColumn: 0,
269
+ endColumn: 0,
270
+ isExported: false,
271
+ signature: sigParts.join('; '),
272
+ updatedAt: now,
273
+ });
274
+ references.push({
275
+ fromNodeId: routeId,
276
+ referenceName: pageStem(entry.pageSourceFile),
277
+ referenceKind: 'references',
278
+ line,
279
+ column: 0,
280
+ filePath,
281
+ language: 'yaml',
282
+ candidates: [pageRel],
283
+ });
284
+ if (entry.buildFunction) {
285
+ references.push({
286
+ fromNodeId: routeId,
287
+ referenceName: entry.buildFunction,
288
+ referenceKind: 'references',
289
+ line,
290
+ column: 0,
291
+ filePath,
292
+ language: 'yaml',
293
+ candidates: [pageRel],
294
+ });
295
+ }
296
+ }
297
+ return { nodes, references };
298
+ }
299
+ /**
300
+ * Spec 0041 — collect `{ name, value }` string resource entries (no edges).
301
+ * Same shape as literal-evidence: walk objects; skip illegal keys.
302
+ */
303
+ function parseHarmonyStringResources(content) {
304
+ const data = (0, jsonc_parser_1.parse)(content, undefined, { allowTrailingComma: true });
305
+ if (data === undefined || data === null)
306
+ return [];
307
+ const out = [];
308
+ let visited = 0;
309
+ const visit = (value, depth) => {
310
+ if (++visited > 10000 || depth > 12 || !value || typeof value !== 'object')
311
+ return;
312
+ if (Array.isArray(value)) {
313
+ for (const item of value)
314
+ visit(item, depth + 1);
315
+ return;
316
+ }
317
+ const entry = value;
318
+ if (typeof entry.name === 'string'
319
+ && typeof entry.value === 'string'
320
+ && /^[A-Za-z_][\w]*$/.test(entry.name)
321
+ && out.length < STRING_JSON_ENTRY_CAP) {
322
+ const needle = JSON.stringify(entry.name);
323
+ const idx = content.indexOf(needle);
324
+ const line = idx < 0 ? 1 : content.slice(0, idx).split('\n').length;
325
+ out.push({
326
+ name: entry.name,
327
+ value: entry.value.slice(0, STRING_JSON_VALUE_CAP),
328
+ line,
329
+ });
330
+ }
331
+ for (const child of Object.values(entry))
332
+ visit(child, depth + 1);
333
+ };
334
+ visit(data, 0);
335
+ return out;
336
+ }
337
+ function extractHarmonyStringJson(filePath, content, now) {
338
+ const nodes = [];
339
+ for (const entry of parseHarmonyStringResources(content)) {
340
+ nodes.push({
341
+ id: `harmony-string:${filePath}:${entry.name}`,
342
+ kind: 'constant',
343
+ name: entry.name,
344
+ qualifiedName: `app.string.${entry.name}`,
345
+ filePath,
346
+ language: 'yaml',
347
+ startLine: entry.line,
348
+ endLine: entry.line,
349
+ startColumn: 0,
350
+ endColumn: 0,
351
+ isExported: false,
352
+ docstring: entry.value,
353
+ signature: `$r('app.string.${entry.name}')`,
354
+ updatedAt: now,
355
+ });
356
+ }
357
+ return { nodes, references: [] };
358
+ }
359
+ /** Spec 0048 — form_config.json `forms[].name`. */
360
+ function parseHarmonyFormConfig(content) {
361
+ let data;
362
+ try {
363
+ data = JSON.parse(content);
364
+ }
365
+ catch {
366
+ try {
367
+ data = (0, jsonc_parser_1.parse)(content, undefined, { allowTrailingComma: true });
368
+ }
369
+ catch {
370
+ return [];
371
+ }
372
+ }
373
+ if (!data || typeof data !== 'object')
374
+ return [];
375
+ const forms = data.forms;
376
+ if (!Array.isArray(forms))
377
+ return [];
378
+ const out = [];
379
+ for (const item of forms) {
380
+ if (out.length >= CAPABILITY_ENTRY_CAP)
381
+ break;
382
+ if (!item || typeof item !== 'object')
383
+ continue;
384
+ const name = typeof item.name === 'string'
385
+ ? item.name.trim()
386
+ : '';
387
+ if (!name)
388
+ continue;
389
+ const needle = JSON.stringify(name);
390
+ const idx = content.indexOf(needle);
391
+ const line = idx < 0 ? 1 : content.slice(0, idx).split('\n').length;
392
+ out.push({ name, line });
393
+ }
394
+ return out;
395
+ }
396
+ /** Spec 0048 — shortcuts_config.json `shortcuts[]`. */
397
+ function parseHarmonyShortcutsConfig(content) {
398
+ let data;
399
+ try {
400
+ data = JSON.parse(content);
401
+ }
402
+ catch {
403
+ try {
404
+ data = (0, jsonc_parser_1.parse)(content, undefined, { allowTrailingComma: true });
405
+ }
406
+ catch {
407
+ return [];
408
+ }
409
+ }
410
+ if (!data || typeof data !== 'object')
411
+ return [];
412
+ const shortcuts = data.shortcuts;
413
+ if (!Array.isArray(shortcuts))
414
+ return [];
415
+ const out = [];
416
+ for (const item of shortcuts) {
417
+ if (out.length >= CAPABILITY_ENTRY_CAP)
418
+ break;
419
+ if (!item || typeof item !== 'object')
420
+ continue;
421
+ const rec = item;
422
+ const shortcutId = typeof rec.shortcutId === 'string' ? rec.shortcutId.trim() : '';
423
+ if (!shortcutId)
424
+ continue;
425
+ const label = typeof rec.label === 'string' ? rec.label.trim() : '';
426
+ let abilityName;
427
+ if (Array.isArray(rec.wants) && rec.wants[0] && typeof rec.wants[0] === 'object') {
428
+ const w = rec.wants[0];
429
+ if (typeof w.abilityName === 'string' && w.abilityName.trim()) {
430
+ abilityName = w.abilityName.trim();
431
+ }
432
+ }
433
+ const needle = JSON.stringify(shortcutId);
434
+ const idx = content.indexOf(needle);
435
+ const line = idx < 0 ? 1 : content.slice(0, idx).split('\n').length;
436
+ out.push({ shortcutId, label, abilityName, line });
437
+ }
438
+ return out;
439
+ }
440
+ function extractHarmonyFormConfig(filePath, content, now) {
441
+ const nodes = [];
442
+ for (const entry of parseHarmonyFormConfig(content)) {
443
+ nodes.push({
444
+ id: `harmony-form:${filePath}:${entry.name}`,
445
+ kind: 'constant',
446
+ name: entry.name,
447
+ qualifiedName: `harmony.form.${entry.name}`,
448
+ filePath,
449
+ language: 'yaml',
450
+ startLine: entry.line,
451
+ endLine: entry.line,
452
+ startColumn: 0,
453
+ endColumn: 0,
454
+ isExported: false,
455
+ docstring: 'form_config',
456
+ signature: `form_config name=${entry.name}`,
457
+ updatedAt: now,
458
+ });
459
+ }
460
+ return { nodes, references: [] };
461
+ }
462
+ function extractHarmonyShortcutsConfig(filePath, content, now) {
463
+ const nodes = [];
464
+ for (const entry of parseHarmonyShortcutsConfig(content)) {
465
+ const sigParts = [`shortcuts_config id=${entry.shortcutId}`];
466
+ if (entry.label)
467
+ sigParts.push(`label=${entry.label}`);
468
+ if (entry.abilityName)
469
+ sigParts.push(`ability=${entry.abilityName}`);
470
+ nodes.push({
471
+ id: `harmony-shortcut:${filePath}:${entry.shortcutId}`,
472
+ kind: 'constant',
473
+ name: entry.shortcutId,
474
+ qualifiedName: `harmony.shortcut.${entry.shortcutId}`,
475
+ filePath,
476
+ language: 'yaml',
477
+ startLine: entry.line,
478
+ endLine: entry.line,
479
+ startColumn: 0,
480
+ endColumn: 0,
481
+ isExported: false,
482
+ docstring: entry.label || 'shortcut',
483
+ signature: sigParts.join('; '),
484
+ updatedAt: now,
485
+ });
486
+ }
487
+ return { nodes, references: [] };
488
+ }
489
+ /** Spec 0048 — module.json5 form extensions + shortcuts/form metadata. */
490
+ function extractModuleCapabilityWiring(filePath, content, now, nodes) {
491
+ // Locate `"type": "form"` then take the nearest preceding `"name"` in a 500-char window.
492
+ for (const m of content.matchAll(/"type"\s*:\s*"form"/g)) {
493
+ const start = Math.max(0, (m.index ?? 0) - 500);
494
+ const window = content.slice(start, (m.index ?? 0) + m[0].length + 200);
495
+ const names = [...window.matchAll(/"name"\s*:\s*"([^"]+)"/g)];
496
+ const name = names.length ? names[names.length - 1][1] : '';
497
+ if (!name || name.startsWith('ohos.'))
498
+ continue;
499
+ const src = /"srcEntry"\s*:\s*"([^"]+)"/.exec(window)?.[1] ?? '';
500
+ const line = lineOfNeedle(content, name);
501
+ const id = `harmony-form-ability:${filePath}:${name}`;
502
+ if (nodes.some((n) => n.id === id))
503
+ continue;
504
+ nodes.push({
505
+ id,
506
+ kind: 'route',
507
+ name: `formAbility:${name}`,
508
+ qualifiedName: `${filePath}::formAbility::${name}`,
509
+ filePath,
510
+ language: 'yaml',
511
+ startLine: line,
512
+ endLine: line,
513
+ startColumn: 0,
514
+ endColumn: 0,
515
+ isExported: false,
516
+ docstring: 'FormExtensionAbility',
517
+ signature: `type=form; srcEntry=${src}`,
518
+ updatedAt: now,
519
+ });
520
+ }
521
+ for (const m of content.matchAll(/\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"resource"\s*:\s*"(\$profile:[^"]+)"\s*\}/g)) {
522
+ const metaName = m[1];
523
+ const resource = m[2];
524
+ const isShortcut = /shortcut/i.test(metaName) || /shortcuts_config/i.test(resource);
525
+ const isForm = /ohos\.extension\.form/i.test(metaName) || /form_config/i.test(resource);
526
+ if (!isShortcut && !isForm)
527
+ continue;
528
+ const kind = isForm ? 'form' : 'shortcut';
529
+ const line = content.slice(0, m.index ?? 0).split('\n').length;
530
+ nodes.push({
531
+ id: `harmony-capability-meta:${filePath}:${metaName}:${resource}`,
532
+ kind: 'constant',
533
+ name: metaName,
534
+ qualifiedName: `harmony.capability.${kind}.${metaName}`,
535
+ filePath,
536
+ language: 'yaml',
537
+ startLine: line,
538
+ endLine: line,
539
+ startColumn: 0,
540
+ endColumn: 0,
541
+ isExported: false,
542
+ docstring: kind,
543
+ signature: `metadata ${metaName} → ${resource}`,
544
+ updatedAt: now,
545
+ });
546
+ }
547
+ }
89
548
  exports.arktsEntryResolver = {
90
549
  name: 'arkts-entry',
91
550
  languages: ['arkts', 'yaml'],
92
551
  detect(context) {
93
552
  for (const file of context.getAllFiles()) {
94
- if (file.endsWith('module.json5'))
553
+ if (file.endsWith('module.json5')
554
+ || (0, grammars_1.isHarmonyRouteProfileJson)(file)
555
+ || (0, grammars_1.isHarmonyElementStringJson)(file)
556
+ || (0, grammars_1.isHarmonyCapabilityProfileJson)(file))
95
557
  return true;
96
558
  if (!file.endsWith('.ets'))
97
559
  continue;
@@ -102,43 +564,22 @@ exports.arktsEntryResolver = {
102
564
  return false;
103
565
  },
104
566
  claimsReference(name) {
105
- return name.startsWith('pages/') || /^[A-Z][A-Za-z0-9]*$/.test(name);
567
+ return (name.startsWith('pages/') ||
568
+ /^[A-Z][A-Za-z0-9]*$/.test(name) ||
569
+ /Builder$/.test(name));
106
570
  },
107
571
  extract(filePath, content) {
108
572
  const nodes = [];
109
573
  const references = [];
110
574
  const now = Date.now();
575
+ const base = filePath.replace(/\\/g, '/').split('/').pop()?.toLowerCase() ?? '';
111
576
  if (filePath.endsWith('module.json5')) {
112
577
  const pages = parseModuleJson5Pages(content);
113
578
  for (const page of pages) {
114
- const line = content.split('\n').findIndex((l) => l.includes(page)) + 1 || 1;
115
- const routeId = `arkts-route:${filePath}:${page}`;
116
- nodes.push({
117
- id: routeId,
118
- kind: 'route',
119
- name: page,
120
- qualifiedName: `${filePath}::${page}`,
121
- filePath,
122
- language: 'yaml',
123
- startLine: line > 0 ? line : 1,
124
- endLine: line > 0 ? line : 1,
125
- startColumn: 0,
126
- endColumn: 0,
127
- isExported: false,
128
- updatedAt: now,
129
- });
130
- references.push({
131
- fromNodeId: routeId,
132
- referenceName: pageStem(page),
133
- referenceKind: 'references',
134
- line: line > 0 ? line : 1,
135
- column: 0,
136
- filePath,
137
- language: 'yaml',
138
- });
579
+ pushPageRoute(nodes, references, filePath, content, page, now);
139
580
  }
140
581
  for (const ability of parseModuleAbilities(content)) {
141
- const line = content.split('\n').findIndex((l) => l.includes(ability.name)) + 1 || 1;
582
+ const line = lineOfNeedle(content, ability.name);
142
583
  const abilityId = `arkts-ability:${filePath}:${ability.name}`;
143
584
  nodes.push({
144
585
  id: abilityId,
@@ -147,8 +588,8 @@ exports.arktsEntryResolver = {
147
588
  qualifiedName: `${filePath}::ability::${ability.name}`,
148
589
  filePath,
149
590
  language: 'yaml',
150
- startLine: line > 0 ? line : 1,
151
- endLine: line > 0 ? line : 1,
591
+ startLine: line,
592
+ endLine: line,
152
593
  startColumn: 0,
153
594
  endColumn: 0,
154
595
  isExported: false,
@@ -156,8 +597,27 @@ exports.arktsEntryResolver = {
156
597
  updatedAt: now,
157
598
  });
158
599
  }
600
+ extractModuleCapabilityWiring(filePath, content, now, nodes);
601
+ return { nodes, references };
602
+ }
603
+ if (base === 'route_map.json' || base === 'router_map.json') {
604
+ return extractRouterMapProfile(filePath, content, now);
605
+ }
606
+ if (base === 'main_pages.json') {
607
+ for (const page of parseHarmonyMainPages(content)) {
608
+ pushPageRoute(nodes, references, filePath, content, page, now);
609
+ }
159
610
  return { nodes, references };
160
611
  }
612
+ if ((0, grammars_1.isHarmonyElementStringJson)(filePath)) {
613
+ return extractHarmonyStringJson(filePath, content, now);
614
+ }
615
+ if (base === 'form_config.json') {
616
+ return extractHarmonyFormConfig(filePath, content, now);
617
+ }
618
+ if (base === 'shortcuts_config.json') {
619
+ return extractHarmonyShortcutsConfig(filePath, content, now);
620
+ }
161
621
  if (!filePath.endsWith('.ets')) {
162
622
  return { nodes, references };
163
623
  }
@@ -181,6 +641,18 @@ exports.arktsEntryResolver = {
181
641
  resolve(ref, context) {
182
642
  if (ref.referenceKind !== 'references')
183
643
  return null;
644
+ const preferredFile = ref.candidates?.[0]?.replace(/\\/g, '/');
645
+ if (preferredFile) {
646
+ const inFile = findArktsSymbolInFile(context, preferredFile, ref.referenceName);
647
+ if (inFile) {
648
+ return {
649
+ original: ref,
650
+ targetNodeId: inFile.id,
651
+ confidence: 0.92,
652
+ resolvedBy: 'framework',
653
+ };
654
+ }
655
+ }
184
656
  if (ref.referenceName.startsWith('pages/')) {
185
657
  const routesIter = context.iterateNodesByKind?.('route') ?? context.getNodesByKind('route');
186
658
  for (const n of routesIter) {
@@ -204,6 +676,17 @@ exports.arktsEntryResolver = {
204
676
  }
205
677
  return null;
206
678
  }
679
+ if (/Builder$/.test(ref.referenceName)) {
680
+ const builder = findArktsBuilderByName(context, ref.referenceName);
681
+ if (builder) {
682
+ return {
683
+ original: ref,
684
+ targetNodeId: builder.id,
685
+ confidence: 0.85,
686
+ resolvedBy: 'framework',
687
+ };
688
+ }
689
+ }
207
690
  const component = findArktsComponentByName(context, ref.referenceName);
208
691
  if (component) {
209
692
  return {
@@ -56,6 +56,7 @@ const go_module_1 = require("./go-module");
56
56
  const workspace_packages_1 = require("./workspace-packages");
57
57
  const errors_1 = require("../errors");
58
58
  const lru_cache_1 = require("./lru-cache");
59
+ const grammars_1 = require("../extraction/grammars");
59
60
  /** Node kinds that can declare supertypes (extends/implements). */
60
61
  const SUPERTYPE_BEARING_KINDS = new Set([
61
62
  'class', 'struct', 'interface', 'trait', 'protocol', 'enum',
@@ -1087,27 +1088,30 @@ class ReferenceResolver {
1087
1088
  kind,
1088
1089
  line: ref.original.line,
1089
1090
  column: ref.original.column,
1090
- metadata: {
1091
- confidence: ref.confidence,
1092
- resolvedBy: ref.resolvedBy,
1093
- // The ORIGINAL reference text (and kind, when edge-kind promotion
1094
- // rewrote it — calls→instantiates, extends→implements,
1095
- // function_ref→references). If this edge's target is later removed
1096
- // by a re-index, the edge is resurrected as exactly this ref and
1097
- // re-resolved (#1240 removal case) — a faithful resurrection, so
1098
- // re-resolution can never bind anywhere a full re-index wouldn't.
1099
- // Reconstruction from the target node's name instead would strip
1100
- // receiver/qualifier context (`h.greet` → `greet`) and risk a
1101
- // wrong rebind; edges without refName (pre-#1240, synthesized) are
1102
- // deliberately NOT resurrected for the same reason.
1103
- refName: ref.original.referenceName,
1104
- ...(ref.original.referenceKind !== kind ? { refKind: ref.original.referenceKind } : {}),
1105
- // Uniform marker for function-as-value edges (#756), regardless of
1106
- // which strategy resolved them (import vs matchFunctionRef) — lets
1107
- // tooling label "callback registration" and lets validation diff
1108
- // exactly the edges this feature added.
1109
- ...(ref.original.referenceKind === 'function_ref' ? { fnRef: true } : {}),
1110
- },
1091
+ // Spec 0039: Harmony route configs → page/builder edges need heuristic
1092
+ // provenance + registeredAt so explore/trail show the JSON wiring site.
1093
+ ...(() => {
1094
+ const sourceNode = this.queries.getNodeById(ref.original.fromNodeId);
1095
+ const isRouteMap = sourceNode?.kind === 'route' &&
1096
+ (0, grammars_1.isHarmonyRouteConfigFile)(sourceNode.filePath);
1097
+ const baseMeta = {
1098
+ confidence: ref.confidence,
1099
+ resolvedBy: ref.resolvedBy,
1100
+ refName: ref.original.referenceName,
1101
+ ...(ref.original.referenceKind !== kind ? { refKind: ref.original.referenceKind } : {}),
1102
+ ...(ref.original.referenceKind === 'function_ref' ? { fnRef: true } : {}),
1103
+ };
1104
+ if (!isRouteMap)
1105
+ return { metadata: baseMeta };
1106
+ return {
1107
+ provenance: 'heuristic',
1108
+ metadata: {
1109
+ ...baseMeta,
1110
+ synthesizedBy: 'arkts-route-map',
1111
+ registeredAt: `${sourceNode.filePath.replace(/\\/g, '/')}:${ref.original.line}`,
1112
+ },
1113
+ };
1114
+ })(),
1111
1115
  };
1112
1116
  });
1113
1117
  }