homegraph 1.6.2 → 1.6.4

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.
@@ -47,6 +47,7 @@ const function_ref_1 = require("./function-ref");
47
47
  const generated_detection_1 = require("./generated-detection");
48
48
  const languages_1 = require("./languages");
49
49
  const c_cpp_1 = require("./languages/c-cpp");
50
+ const jsdoc_type_tags_1 = require("./jsdoc-type-tags");
50
51
  const liquid_extractor_1 = require("./liquid-extractor");
51
52
  const razor_extractor_1 = require("./razor-extractor");
52
53
  const svelte_extractor_1 = require("./svelte-extractor");
@@ -1511,6 +1512,8 @@ class TreeSitterExtractor {
1511
1512
  return;
1512
1513
  // Extract type annotations (parameter types and return type)
1513
1514
  this.extractTypeAnnotations(node, funcNode.id);
1515
+ // Spec 0066: JSDoc @returns/@param when the signature omitted types.
1516
+ this.emitJsdocTypeFallbacks(funcNode.id, docstring);
1514
1517
  // Extract decorators applied to the function (rare in JS/TS but
1515
1518
  // present in Python `@decorator def f():` and Java/Kotlin
1516
1519
  // annotations on free functions).
@@ -1712,6 +1715,8 @@ class TreeSitterExtractor {
1712
1715
  }
1713
1716
  // Extract type annotations (parameter types and return type)
1714
1717
  this.extractTypeAnnotations(node, methodNode.id);
1718
+ // Spec 0066: JSDoc @returns/@param when the signature omitted types.
1719
+ this.emitJsdocTypeFallbacks(methodNode.id, docstring);
1715
1720
  // Extract decorators (`@Get('/list') list() {}`).
1716
1721
  this.extractDecoratorsFor(node, methodNode.id);
1717
1722
  // Push to stack and visit body
@@ -5636,7 +5641,8 @@ class TreeSitterExtractor {
5636
5641
  ]);
5637
5642
  /**
5638
5643
  * Extract type references from type annotations on a function/method/field node.
5639
- * Creates 'references' edges for parameter types, return types, and field types.
5644
+ * Spec 0066: parameter / field types → `type_of`; return types → `returns`.
5645
+ * Dart / Scala mixed signature walks stay `references` (out of this Spec).
5640
5646
  */
5641
5647
  extractTypeAnnotations(node, nodeId) {
5642
5648
  if (!this.extractor)
@@ -5668,6 +5674,7 @@ class TreeSitterExtractor {
5668
5674
  // `type_identifier` child, not a `type` field — so getChildByField below
5669
5675
  // finds neither. Walk the inner signature: param names / the method name are
5670
5676
  // `identifier` (not `type_identifier`), so only types surface.
5677
+ // Spec 0066: leave as `references` (mixed return+param walk).
5671
5678
  if (this.language === 'dart') {
5672
5679
  let sig = node;
5673
5680
  if (node.type === 'method_signature') {
@@ -5677,7 +5684,7 @@ class TreeSitterExtractor {
5677
5684
  c.type === 'constructor_signature' ||
5678
5685
  c.type === 'factory_constructor_signature') ?? node;
5679
5686
  }
5680
- this.extractTypeRefsFromSubtree(sig, nodeId);
5687
+ this.extractTypeRefsFromSubtree(sig, nodeId, 'references');
5681
5688
  return;
5682
5689
  }
5683
5690
  // Extract parameter type annotations. Scala curries — `def f(a)(implicit
@@ -5687,19 +5694,19 @@ class TreeSitterExtractor {
5687
5694
  if (this.language === 'scala') {
5688
5695
  for (const pc of node.namedChildren) {
5689
5696
  if (pc.type === 'parameters')
5690
- this.extractTypeRefsFromSubtree(pc, nodeId);
5697
+ this.extractTypeRefsFromSubtree(pc, nodeId, 'type_of');
5691
5698
  }
5692
5699
  }
5693
5700
  else {
5694
5701
  const params = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.paramsField || 'parameters');
5695
5702
  if (params) {
5696
- this.extractTypeRefsFromSubtree(params, nodeId);
5703
+ this.extractTypeRefsFromSubtree(params, nodeId, 'type_of');
5697
5704
  }
5698
5705
  }
5699
5706
  // Extract return type annotation
5700
5707
  const returnType = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.returnField || 'return_type');
5701
5708
  if (returnType) {
5702
- this.extractTypeRefsFromSubtree(returnType, nodeId);
5709
+ this.extractTypeRefsFromSubtree(returnType, nodeId, 'returns');
5703
5710
  }
5704
5711
  // Scala context bounds / type-parameter bounds: `def f[A: Monoid]`,
5705
5712
  // `[F[_]: Monad]`, `[A <: Foo]` carry the bound type inside `type_parameters`.
@@ -5711,13 +5718,46 @@ class TreeSitterExtractor {
5711
5718
  if (this.language === 'scala') {
5712
5719
  const typeParams = node.namedChildren.find((c) => c.type === 'type_parameters');
5713
5720
  if (typeParams) {
5714
- this.extractTypeRefsFromSubtree(typeParams, nodeId);
5721
+ this.extractTypeRefsFromSubtree(typeParams, nodeId, 'type_of');
5715
5722
  }
5716
5723
  }
5717
5724
  // Extract direct type annotation (for class fields like `model: ITextModel`)
5718
5725
  const typeAnnotation = node.namedChildren.find((c) => c.type === 'type_annotation');
5719
5726
  if (typeAnnotation) {
5720
- this.extractTypeRefsFromSubtree(typeAnnotation, nodeId);
5727
+ this.extractTypeRefsFromSubtree(typeAnnotation, nodeId, 'type_of');
5728
+ }
5729
+ }
5730
+ /**
5731
+ * Spec 0066: when the AST omitted return/param types, emit unresolved
5732
+ * `returns` / `type_of` refs from JSDoc `@returns` / `@param {Type}`.
5733
+ */
5734
+ emitJsdocTypeFallbacks(fromNodeId, docstring) {
5735
+ if (!docstring)
5736
+ return;
5737
+ const hasReturns = this.unresolvedReferences.some((r) => r.fromNodeId === fromNodeId && r.referenceKind === 'returns');
5738
+ if (!hasReturns) {
5739
+ const ret = (0, jsdoc_type_tags_1.parseJsdocReturns)(docstring);
5740
+ if (ret) {
5741
+ this.unresolvedReferences.push({
5742
+ fromNodeId,
5743
+ referenceName: ret,
5744
+ referenceKind: 'returns',
5745
+ line: 1,
5746
+ column: 0,
5747
+ });
5748
+ }
5749
+ }
5750
+ const hasTypeOf = this.unresolvedReferences.some((r) => r.fromNodeId === fromNodeId && r.referenceKind === 'type_of');
5751
+ if (!hasTypeOf) {
5752
+ for (const p of (0, jsdoc_type_tags_1.parseJsdocParams)(docstring)) {
5753
+ this.unresolvedReferences.push({
5754
+ fromNodeId,
5755
+ referenceName: p.typeName,
5756
+ referenceKind: 'type_of',
5757
+ line: 1,
5758
+ column: 0,
5759
+ });
5760
+ }
5721
5761
  }
5722
5762
  }
5723
5763
  /**
@@ -5923,21 +5963,22 @@ class TreeSitterExtractor {
5923
5963
  // Find type_annotation child (covers TS `: Type`, Rust `: Type`, etc.)
5924
5964
  const typeAnnotation = node.namedChildren.find((c) => c.type === 'type_annotation');
5925
5965
  if (typeAnnotation) {
5926
- this.extractTypeRefsFromSubtree(typeAnnotation, nodeId);
5966
+ this.extractTypeRefsFromSubtree(typeAnnotation, nodeId, 'type_of');
5927
5967
  }
5928
5968
  }
5929
5969
  /**
5930
5970
  * Recursively walk a subtree and extract all type_identifier references.
5931
5971
  * Handles unions, intersections, generics, arrays, etc.
5972
+ * Spec 0066: callers pass `returns` / `type_of` for signature positions.
5932
5973
  */
5933
- extractTypeRefsFromSubtree(node, fromNodeId) {
5974
+ extractTypeRefsFromSubtree(node, fromNodeId, referenceKind = 'references') {
5934
5975
  if (node.type === 'type_identifier') {
5935
5976
  const typeName = (0, tree_sitter_helpers_1.getNodeText)(node, this.source);
5936
5977
  if (typeName && !this.BUILTIN_TYPES.has(typeName)) {
5937
5978
  this.unresolvedReferences.push({
5938
5979
  fromNodeId,
5939
5980
  referenceName: typeName,
5940
- referenceKind: 'references',
5981
+ referenceKind,
5941
5982
  line: node.startPosition.row + 1,
5942
5983
  column: node.startPosition.column,
5943
5984
  });
@@ -5948,7 +5989,7 @@ class TreeSitterExtractor {
5948
5989
  for (let i = 0; i < node.namedChildCount; i++) {
5949
5990
  const child = node.namedChild(i);
5950
5991
  if (child) {
5951
- this.extractTypeRefsFromSubtree(child, fromNodeId);
5992
+ this.extractTypeRefsFromSubtree(child, fromNodeId, referenceKind);
5952
5993
  }
5953
5994
  }
5954
5995
  }
package/dist/index.js CHANGED
@@ -490,7 +490,37 @@ class HomeGraph {
490
490
  this.db.beginBulkParseLoad();
491
491
  let result;
492
492
  try {
493
- result = await this.orchestrator.indexAll(options.onProgress, options.signal, options.verbose, walValve ? () => walValve.backpressure() : undefined, freshDb ? { dbPath: this.db.getPath(), fastInit } : null);
493
+ result = await this.orchestrator.indexAll(options.onProgress, options.signal, options.verbose, walValve ? () => walValve.backpressure() : undefined, freshDb
494
+ ? {
495
+ dbPath: this.db.getPath(),
496
+ fastInit,
497
+ // Spec 0065: exclusive store-worker handoff (close main → worker → reopen).
498
+ exclusiveHandoff: {
499
+ release: () => {
500
+ this.db.closeForExclusiveWriter();
501
+ },
502
+ reclaim: () => {
503
+ this.db.reopenAfterExclusiveWriter({ fastInit });
504
+ this.queries.rebind(this.db.getDb());
505
+ (0, context_1.bindExtractionContext)(this.projectRoot, this.queries);
506
+ // ATTACH is per-connection. ohosApiDbPath survives rebind and
507
+ // would make attachOhosApiDb early-return while the schema
508
+ // is gone → "no such table: ohos_api.nodes". Clear then reattach.
509
+ const priorOhos = this.queries.getOhosApiDbPath();
510
+ this.queries.attachOhosApiDb(null);
511
+ if (priorOhos) {
512
+ this.queries.attachOhosApiDb(priorOhos);
513
+ }
514
+ else if ((0, graph_sources_1.graphSourceFlags)(this.graphSources).sdk) {
515
+ (0, arkts_1.restoreOhosApiDbAttach)(this.queries);
516
+ if (!this.queries.getOhosApiDbPath()) {
517
+ (0, arkts_1.attachExistingOhosApiDbForProject)(this.projectRoot, this.queries);
518
+ }
519
+ }
520
+ },
521
+ },
522
+ }
523
+ : null);
494
524
  }
495
525
  finally {
496
526
  if (freshDb) {
@@ -25,6 +25,18 @@ import type { Edge } from '../types';
25
25
  import type { QueryBuilder } from '../db/queries';
26
26
  import type { ResolutionContext } from './types';
27
27
  import { type MaybeYield } from './cooperative-yield';
28
+ /**
29
+ * Spec 0045 A — ArkUI parent build/@Builder → child @Component/@Builder.
30
+ * Analog of react jsx-child: only static PascalCase call names that resolve to
31
+ * a unique in-repo component/builder; ambiguous names are dropped.
32
+ *
33
+ * Scan the enclosing @Component/@Builder struct (or Builder function) body —
34
+ * not only the `build()` method span — because ArkUI trailing lambdas
35
+ * (`Column() { Child() }`) are often attributed with tight method ranges that
36
+ * omit nested call sites.
37
+ */
38
+ /** @internal exported for PERF kind-scan regression tests (resolveChild name-miss path). */
39
+ export declare function arkuiChildComponentEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]>;
28
40
  /** `has(...)` shape passed to pass gates — true when the project contains any of the languages. */
29
41
  type HasLang = (...ls: string[]) => boolean;
30
42
  /**
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.SYNTH_PROGRESS_STEPS = exports.SYNTH_PASSES = void 0;
37
+ exports.arkuiChildComponentEdges = arkuiChildComponentEdges;
37
38
  exports.synthesizeCallbackEdges = synthesizeCallbackEdges;
38
39
  const generated_detection_1 = require("../extraction/generated-detection");
39
40
  const strip_comments_1 = require("./strip-comments");
@@ -1018,6 +1019,7 @@ function isArkuiBuildParent(n) {
1018
1019
  * (`Column() { Child() }`) are often attributed with tight method ranges that
1019
1020
  * omit nested call sites.
1020
1021
  */
1022
+ /** @internal exported for PERF kind-scan regression tests (resolveChild name-miss path). */
1021
1023
  async function arkuiChildComponentEdges(ctx, onYield) {
1022
1024
  const edges = [];
1023
1025
  const seen = new Set();
@@ -1033,15 +1035,10 @@ async function arkuiChildComponentEdges(ctx, onYield) {
1033
1035
  return '';
1034
1036
  };
1035
1037
  const resolveChild = (name, fromFile, parentId) => {
1038
+ // Name index only — miss returns null. Framework PascalCase calls
1039
+ // (Column/Button/Text/…) dominate misses and must not full-scan kinds
1040
+ // (Spec 0063). Same drop policy as ambiguous multi-match below.
1036
1041
  let candidates = ctx.getNodesByName(name).filter(isArkuiChildTarget);
1037
- if (candidates.length === 0) {
1038
- candidates = [
1039
- ...ctx.getNodesByKind('struct'),
1040
- ...ctx.getNodesByKind('component'),
1041
- ...ctx.getNodesByKind('function'),
1042
- ...ctx.getNodesByKind('method'),
1043
- ].filter((n) => n.name === name && isArkuiChildTarget(n));
1044
- }
1045
1042
  candidates = candidates.filter((n) => n.id !== parentId);
1046
1043
  // Dedupe by id — name index can surface the same row twice across caches.
1047
1044
  candidates = [...new Map(candidates.map((n) => [n.id, n])).values()];
@@ -30,6 +30,11 @@ export declare function isKnownLanguageFamily(lang: string): boolean;
30
30
  * returns `false` here and is left alone.
31
31
  */
32
32
  export declare function crossesKnownFamily(a: string, b: string): boolean;
33
+ /**
34
+ * Spec 0066: resolve a return/param type name to a unique type node.
35
+ * Ambiguous same-named types → no edge (wrong type hub is worse than none).
36
+ */
37
+ export declare function matchTypeEdge(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
33
38
  /**
34
39
  * Resolve a function-as-value reference (#756) — a function name used as a
35
40
  * callback/function-pointer value (`register(handler)`, `o->cb = handler`,
@@ -9,6 +9,7 @@ exports.matchByFilePath = matchByFilePath;
9
9
  exports.sameLanguageFamily = sameLanguageFamily;
10
10
  exports.isKnownLanguageFamily = isKnownLanguageFamily;
11
11
  exports.crossesKnownFamily = crossesKnownFamily;
12
+ exports.matchTypeEdge = matchTypeEdge;
12
13
  exports.matchFunctionRef = matchFunctionRef;
13
14
  exports.matchByExactName = matchByExactName;
14
15
  exports.matchByQualifiedName = matchByQualifiedName;
@@ -193,7 +194,10 @@ function crossesKnownFamily(a, b) {
193
194
  * both-known filter so `.vue`/`.svelte` (own tag) importing `.ts` survives.
194
195
  */
195
196
  function applyLanguageGate(candidates, ref) {
196
- if (ref.referenceKind === 'references' || ref.referenceKind === 'function_ref') {
197
+ if (ref.referenceKind === 'references' ||
198
+ ref.referenceKind === 'function_ref' ||
199
+ ref.referenceKind === 'returns' ||
200
+ ref.referenceKind === 'type_of') {
197
201
  return candidates.filter((c) => sameLanguageFamily(c.language, ref.language));
198
202
  }
199
203
  if (ref.referenceKind === 'imports') {
@@ -201,6 +205,57 @@ function applyLanguageGate(candidates, ref) {
201
205
  }
202
206
  return candidates;
203
207
  }
208
+ /** Spec 0066 — targets for `returns` / `type_of` edges (type-as-node). */
209
+ const TYPE_EDGE_KINDS = new Set([
210
+ 'class',
211
+ 'interface',
212
+ 'type_alias',
213
+ 'enum',
214
+ 'struct',
215
+ 'protocol',
216
+ 'trait',
217
+ 'component',
218
+ ]);
219
+ /**
220
+ * Spec 0066: resolve a return/param type name to a unique type node.
221
+ * Ambiguous same-named types → no edge (wrong type hub is worse than none).
222
+ */
223
+ function matchTypeEdge(ref, context) {
224
+ if (ref.referenceKind !== 'returns' && ref.referenceKind !== 'type_of')
225
+ return null;
226
+ const simple = ref.referenceName.includes('.') || ref.referenceName.includes('::')
227
+ ? (ref.referenceName.split(/[.:]+/).filter(Boolean).pop() ?? ref.referenceName)
228
+ : ref.referenceName;
229
+ let candidates = context
230
+ .getNodesByName(simple)
231
+ .filter((n) => TYPE_EDGE_KINDS.has(n.kind) && n.id !== ref.fromNodeId);
232
+ candidates = applyLanguageGate(candidates, ref);
233
+ if (candidates.length === 0)
234
+ return null;
235
+ // Qualified exact: unique_id-style `A.B.Foo` / `A::Foo` against qualifiedName.
236
+ if (ref.referenceName.includes('.') || ref.referenceName.includes('::')) {
237
+ const qnHits = candidates.filter((n) => n.qualifiedName === ref.referenceName ||
238
+ n.qualifiedName.endsWith(`.${simple}`) ||
239
+ n.qualifiedName.endsWith(`::${simple}`));
240
+ if (qnHits.length === 1) {
241
+ return {
242
+ original: ref,
243
+ targetNodeId: qnHits[0].id,
244
+ confidence: 0.95,
245
+ resolvedBy: 'qualified-name',
246
+ };
247
+ }
248
+ }
249
+ const ordered = preferCallSiteFile(candidates, ref.filePath);
250
+ if (ordered.length !== 1)
251
+ return null;
252
+ return {
253
+ original: ref,
254
+ targetNodeId: ordered[0].id,
255
+ confidence: 0.9,
256
+ resolvedBy: 'exact-match',
257
+ };
258
+ }
204
259
  /**
205
260
  * Resolve a function-as-value reference (#756) — a function name used as a
206
261
  * callback/function-pointer value (`register(handler)`, `o->cb = handler`,
@@ -2078,6 +2133,10 @@ function matchReference(ref, context) {
2078
2133
  if (ref.referenceKind === 'function_ref') {
2079
2134
  return matchFunctionRef(ref, context);
2080
2135
  }
2136
+ // Spec 0066: returns / type_of — unique type node only; no fuzzy fallthrough.
2137
+ if (ref.referenceKind === 'returns' || ref.referenceKind === 'type_of') {
2138
+ return matchTypeEdge(ref, context);
2139
+ }
2081
2140
  // ArkTS chained UI attributes — emitted with a leading dot (`.titleStyle`,
2082
2141
  // `.width`) by the extractor — resolve ONLY to decorator-marked attribute
2083
2142
  // helpers: `@Extend`/`@Styles`/`@AnimatableExtend` functions (and global
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homegraph",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,6 +25,8 @@
25
25
  "test:node-matrix": "bash scripts/test-node-matrix.sh",
26
26
  "test:explore-routing": "node scripts/run-explore-routing.mjs",
27
27
  "eval": "npm run build && npx tsx test/evaluation/runner.ts",
28
+ "eval:peak-mem": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types test/evaluation/index-peak-mem.ts",
29
+ "soak:photos": "node test/harmony-callgraph-soak/run.mjs",
28
30
  "clean": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true})\""
29
31
  },
30
32
  "keywords": [