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.
- package/CHANGELOG.md +26 -0
- package/dist/bin/homegraph.js +15 -9
- package/dist/db/index.d.ts +21 -0
- package/dist/db/index.js +56 -0
- package/dist/directory.d.ts +17 -0
- package/dist/directory.js +40 -0
- package/dist/extraction/grammars.js +7 -0
- package/dist/extraction/index.d.ts +5 -0
- package/dist/extraction/index.js +162 -116
- package/dist/extraction/jsdoc-type-tags.d.ts +19 -0
- package/dist/extraction/jsdoc-type-tags.js +115 -0
- package/dist/extraction/languages/arkts.js +198 -31
- package/dist/extraction/store-flush-guard.d.ts +11 -0
- package/dist/extraction/store-flush-guard.js +19 -0
- package/dist/extraction/store-worker.d.ts +3 -2
- package/dist/extraction/store-worker.js +3 -2
- package/dist/extraction/tree-sitter.d.ts +8 -1
- package/dist/extraction/tree-sitter.js +52 -11
- package/dist/index.js +31 -1
- package/dist/resolution/callback-synthesizer.d.ts +12 -0
- package/dist/resolution/callback-synthesizer.js +5 -8
- package/dist/resolution/name-matcher.d.ts +5 -0
- package/dist/resolution/name-matcher.js +60 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,30 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
## [1.6.4] - 2026-11-05
|
|
14
|
+
|
|
15
|
+
### Improvements
|
|
16
|
+
|
|
17
|
+
- Return and parameter types link to type nodes as `returns` / `type_of` edges (unique match only), so a type can be reverse-queried for methods that return or take it. Signature annotations are preferred; JSDoc `@returns` / `@param {Type}` fill gaps when the signature omits types. `nodes.return_type` is unchanged for chained-call inference (Spec 0066).
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
|
|
21
|
+
- Fresh index store-worker: the main DB connection is closed before the worker opens (and reopened after it drains), so ArkTS Phase-A writes and the worker no longer share the file. Stops `SQLITE_BUSY` / 100% CPU hangs on MEMORY or non-WAL backends. Reclaim clears and re-ATTACHes `ohos_api` (stale in-memory path after reopen caused `no such table: ohos_api.nodes`). Store flush failures now abort the feed backpressure loop instead of spinning (Spec 0065).
|
|
22
|
+
- CLI `init` / `index` / `sync`: an explicit directory argument is used as-is. With no argument, the commands walk up from the cwd to the nearest `build-profile.json5` (Harmony project root), otherwise stay at cwd. They no longer adopt an ancestor `.homegraph` from a parent harness or monorepo, which could silently index the wrong tree (Spec 0061).
|
|
23
|
+
- ArkTS: after a modular batch, orphan `.ets` files (outside PROJECT modules; still tree-sitter TypeScript fallback per Spec 0036) no longer fail with `Failed to get parser for language: typescript` on ArkUI-only trees. Indexing preloads the TypeScript WASM whenever `arkts` is in the language set, including on the main thread when parse workers are enabled (Spec 0062).
|
|
24
|
+
- ArkUI `arkui-child` synthesis: when a PascalCase call name misses the name index (typical for framework `Column`/`Button`/`Text`), do not fall back to scanning every `struct`/`component`/`function`/`method` node — return no edge. Cuts large-repo resolveChild CPU (Spec 0063).
|
|
25
|
+
- ArkTS ViewTree lazy method nodes (including `%AM*` handlers): `file_path` and `contains` parent always follow `getDeclaringArkFile()` / declaring class, not the ViewTree importer that first walked the nested tree — so anonymous callbacks no longer drift onto `MainPageView`-style hosts (Spec 0064).
|
|
26
|
+
|
|
27
|
+
## [1.6.3] - 2026-10-22
|
|
28
|
+
|
|
29
|
+
### Improvements
|
|
30
|
+
|
|
31
|
+
- Release workflow jobs are split so a failed stage can be re-run alone: unit tests, CHANGELOG prepare, GitHub Release, npm publish, long registry verify, then OHOS API db packages.
|
|
32
|
+
|
|
33
|
+
### Fixes
|
|
34
|
+
|
|
35
|
+
- ArkTS: cross-file `imports` and `extends`/`implements` edges are no longer dropped during per-file persist. They ride the same `crossFileEdges` path as calls. Built-in ArkUI decorators (`@State`, `@Builder`, …) are not fuzzy-matched onto unrelated same-named symbols (Spec 0059).
|
|
36
|
+
|
|
13
37
|
## [1.6.2] - 2026-10-19
|
|
14
38
|
|
|
15
39
|
### New Features
|
|
@@ -305,3 +329,5 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
305
329
|
[1.6.0]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.0
|
|
306
330
|
[1.6.1]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.1
|
|
307
331
|
[1.6.2]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.2
|
|
332
|
+
[1.6.3]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.3
|
|
333
|
+
[1.6.4]: https://github.com/fujiaxin-coder/homegraph/releases/tag/v1.6.4
|
package/dist/bin/homegraph.js
CHANGED
|
@@ -176,9 +176,12 @@ function main() {
|
|
|
176
176
|
// Helper Functions
|
|
177
177
|
// =============================================================================
|
|
178
178
|
/**
|
|
179
|
-
* Resolve project path
|
|
180
|
-
* Walks up
|
|
181
|
-
*
|
|
179
|
+
* Resolve project path for *read* CLI commands (status, query, …).
|
|
180
|
+
* Walks up to the nearest initialized HomeGraph project (has homegraph.db).
|
|
181
|
+
*
|
|
182
|
+
* Mutating commands (`init` / `index` / `sync`) must NOT use this — they use
|
|
183
|
+
* {@link resolveCliMutatingProjectRoot} (Spec 0061) so an ancestor harness
|
|
184
|
+
* `.homegraph` cannot hijack a nested Harmony project.
|
|
182
185
|
*/
|
|
183
186
|
function resolveProjectPath(pathArg) {
|
|
184
187
|
const absolutePath = path.resolve(pathArg || process.cwd());
|
|
@@ -445,12 +448,13 @@ function main() {
|
|
|
445
448
|
*/
|
|
446
449
|
program
|
|
447
450
|
.command('init [path]')
|
|
448
|
-
.description('Initialize HomeGraph in a project directory and build the initial index')
|
|
451
|
+
.description('Initialize HomeGraph in a project directory and build the initial index (explicit path is literal; bare init uses nearest build-profile.json5)')
|
|
449
452
|
.option('-i, --index', 'Deprecated: indexing now runs by default; flag accepted for backward compatibility')
|
|
450
453
|
.option('-f, --force', 'Initialize even if the path looks like your home directory or a filesystem root')
|
|
451
454
|
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
|
|
452
455
|
.action(async (pathArg, options) => {
|
|
453
|
-
|
|
456
|
+
// Spec 0061: explicit path is literal; bare init walks up to build-profile.json5.
|
|
457
|
+
const projectPath = (0, directory_1.resolveCliMutatingProjectRoot)(pathArg);
|
|
454
458
|
const clack = await importESM('@clack/prompts');
|
|
455
459
|
clack.intro('Initializing HomeGraph');
|
|
456
460
|
try {
|
|
@@ -569,13 +573,14 @@ function main() {
|
|
|
569
573
|
*/
|
|
570
574
|
program
|
|
571
575
|
.command('index [path]')
|
|
572
|
-
.description('Rebuild the full index from scratch (
|
|
576
|
+
.description('Rebuild the full index from scratch (explicit path is literal; bare index uses nearest build-profile.json5)')
|
|
573
577
|
.option('-f, --force', 'Index even if the path looks like your home directory or a filesystem root')
|
|
574
578
|
.option('-q, --quiet', 'Suppress progress output')
|
|
575
579
|
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
|
|
576
580
|
.option('--inplace', 'Index into the existing database without recreating it (MCP auto-init; DB may already be open by the parent)')
|
|
577
581
|
.action(async (pathArg, options) => {
|
|
578
|
-
|
|
582
|
+
// Spec 0061: do not adopt an ancestor .homegraph (harness hijack).
|
|
583
|
+
const projectPath = (0, directory_1.resolveCliMutatingProjectRoot)(pathArg);
|
|
579
584
|
try {
|
|
580
585
|
// Don't (re)index your home directory / a filesystem root (#845). --force
|
|
581
586
|
// doubles as the override.
|
|
@@ -647,10 +652,11 @@ function main() {
|
|
|
647
652
|
*/
|
|
648
653
|
program
|
|
649
654
|
.command('sync [path]')
|
|
650
|
-
.description('Sync changes since last index')
|
|
655
|
+
.description('Sync changes since last index (explicit path is literal; bare sync uses nearest build-profile.json5)')
|
|
651
656
|
.option('-q, --quiet', 'Suppress output (for git hooks)')
|
|
652
657
|
.action(async (pathArg, options) => {
|
|
653
|
-
|
|
658
|
+
// Spec 0061: same root rules as init/index.
|
|
659
|
+
const projectPath = (0, directory_1.resolveCliMutatingProjectRoot)(pathArg);
|
|
654
660
|
try {
|
|
655
661
|
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
656
662
|
if (!options.quiet) {
|
package/dist/db/index.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export declare class DatabaseConnection {
|
|
|
36
36
|
* dead inode forever (#925). See `isReplacedOnDisk`.
|
|
37
37
|
*/
|
|
38
38
|
private openedInode;
|
|
39
|
+
/**
|
|
40
|
+
* True after {@link closeForExclusiveWriter} until {@link reopenAfterExclusiveWriter}.
|
|
41
|
+
* The underlying handle is closed; callers must not use {@link getDb} in this window.
|
|
42
|
+
*/
|
|
43
|
+
private releasedForExclusiveWriter;
|
|
39
44
|
private constructor();
|
|
40
45
|
/**
|
|
41
46
|
* Initialize a new database at the given path
|
|
@@ -334,6 +339,22 @@ export declare class DatabaseConnection {
|
|
|
334
339
|
* Check if the database connection is open
|
|
335
340
|
*/
|
|
336
341
|
isOpen(): boolean;
|
|
342
|
+
/**
|
|
343
|
+
* Close this handle so another writer (fresh-index store-worker) can own the
|
|
344
|
+
* DB file exclusively. On-disk bulk-load state (dropped secondary indexes /
|
|
345
|
+
* missing FTS triggers) is preserved — do NOT use {@link open} to come back.
|
|
346
|
+
* Pair with {@link reopenAfterExclusiveWriter}. Spec 0065.
|
|
347
|
+
*/
|
|
348
|
+
closeForExclusiveWriter(): void;
|
|
349
|
+
/**
|
|
350
|
+
* Re-open after {@link closeForExclusiveWriter}. Skips `open()` healers so a
|
|
351
|
+
* mid-`indexAll` bulk window stays open until `endBulk*`. When `fastInit` is
|
|
352
|
+
* set, restores MEMORY journal + OFF synchronous to match the pre-handoff
|
|
353
|
+
* fresh-index path. Spec 0065.
|
|
354
|
+
*/
|
|
355
|
+
reopenAfterExclusiveWriter(opts: {
|
|
356
|
+
fastInit: boolean;
|
|
357
|
+
}): void;
|
|
337
358
|
/**
|
|
338
359
|
* True when the DB file at our path has been REPLACED on disk since we opened
|
|
339
360
|
* it — a different inode now lives at the same path, so the fd we still hold
|
package/dist/db/index.js
CHANGED
|
@@ -124,6 +124,11 @@ class DatabaseConnection {
|
|
|
124
124
|
* dead inode forever (#925). See `isReplacedOnDisk`.
|
|
125
125
|
*/
|
|
126
126
|
openedInode;
|
|
127
|
+
/**
|
|
128
|
+
* True after {@link closeForExclusiveWriter} until {@link reopenAfterExclusiveWriter}.
|
|
129
|
+
* The underlying handle is closed; callers must not use {@link getDb} in this window.
|
|
130
|
+
*/
|
|
131
|
+
releasedForExclusiveWriter = false;
|
|
127
132
|
constructor(db, dbPath, backend) {
|
|
128
133
|
this.db = db;
|
|
129
134
|
this.dbPath = dbPath;
|
|
@@ -753,14 +758,65 @@ class DatabaseConnection {
|
|
|
753
758
|
* Close the database connection
|
|
754
759
|
*/
|
|
755
760
|
close() {
|
|
761
|
+
this.releasedForExclusiveWriter = false;
|
|
756
762
|
this.db.close();
|
|
757
763
|
}
|
|
758
764
|
/**
|
|
759
765
|
* Check if the database connection is open
|
|
760
766
|
*/
|
|
761
767
|
isOpen() {
|
|
768
|
+
if (this.releasedForExclusiveWriter)
|
|
769
|
+
return false;
|
|
762
770
|
return this.db.open;
|
|
763
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Close this handle so another writer (fresh-index store-worker) can own the
|
|
774
|
+
* DB file exclusively. On-disk bulk-load state (dropped secondary indexes /
|
|
775
|
+
* missing FTS triggers) is preserved — do NOT use {@link open} to come back.
|
|
776
|
+
* Pair with {@link reopenAfterExclusiveWriter}. Spec 0065.
|
|
777
|
+
*/
|
|
778
|
+
closeForExclusiveWriter() {
|
|
779
|
+
if (this.releasedForExclusiveWriter)
|
|
780
|
+
return;
|
|
781
|
+
this.db.close();
|
|
782
|
+
this.releasedForExclusiveWriter = true;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Re-open after {@link closeForExclusiveWriter}. Skips `open()` healers so a
|
|
786
|
+
* mid-`indexAll` bulk window stays open until `endBulk*`. When `fastInit` is
|
|
787
|
+
* set, restores MEMORY journal + OFF synchronous to match the pre-handoff
|
|
788
|
+
* fresh-index path. Spec 0065.
|
|
789
|
+
*/
|
|
790
|
+
reopenAfterExclusiveWriter(opts) {
|
|
791
|
+
if (!this.releasedForExclusiveWriter) {
|
|
792
|
+
throw new Error('reopenAfterExclusiveWriter: connection was not released for exclusive writer');
|
|
793
|
+
}
|
|
794
|
+
const { db, backend } = (0, sqlite_adapter_1.createDatabase)(this.dbPath);
|
|
795
|
+
// Mirror configureConnection essentials without healBulkNodeLoad / migrations.
|
|
796
|
+
db.pragma('busy_timeout = 5000');
|
|
797
|
+
db.pragma('foreign_keys = ON');
|
|
798
|
+
if (opts.fastInit) {
|
|
799
|
+
try {
|
|
800
|
+
db.pragma('journal_mode = MEMORY');
|
|
801
|
+
db.pragma('synchronous = OFF');
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
/* keep whatever the backend allows */
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
db.pragma('journal_mode = WAL');
|
|
809
|
+
db.pragma('synchronous = NORMAL');
|
|
810
|
+
}
|
|
811
|
+
db.pragma('cache_size = -16000');
|
|
812
|
+
db.pragma('temp_store = MEMORY');
|
|
813
|
+
db.pragma('mmap_size = 0');
|
|
814
|
+
db.pragma(`journal_size_limit = ${exports.WAL_HEAL_THRESHOLD_BYTES}`);
|
|
815
|
+
this.db = db;
|
|
816
|
+
this.backend = backend;
|
|
817
|
+
this.openedInode = statInode(this.dbPath);
|
|
818
|
+
this.releasedForExclusiveWriter = false;
|
|
819
|
+
}
|
|
764
820
|
/**
|
|
765
821
|
* True when the DB file at our path has been REPLACED on disk since we opened
|
|
766
822
|
* it — a different inode now lives at the same path, so the fd we still hold
|
package/dist/directory.d.ts
CHANGED
|
@@ -63,6 +63,23 @@ export declare function isInitialized(projectRoot: string): boolean;
|
|
|
63
63
|
* The returned string is a human phrase that slots into "… looks like {reason}".
|
|
64
64
|
*/
|
|
65
65
|
export declare function unsafeIndexRootReason(projectRoot: string): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Walk up from `startPath` looking for a HarmonyOS project root
|
|
68
|
+
* (`build-profile.json5`). Returns null if none is found before the
|
|
69
|
+
* filesystem root. Spec 0061.
|
|
70
|
+
*/
|
|
71
|
+
export declare function findNearestHarmonyProjectRoot(startPath: string): string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the project root for CLI mutating commands (`init` / `index` / `sync`).
|
|
74
|
+
*
|
|
75
|
+
* - Explicit `pathArg` → `path.resolve(cwd, pathArg)` literally (no walk-up).
|
|
76
|
+
* - Omitted / empty → nearest ancestor (or cwd) with `build-profile.json5`,
|
|
77
|
+
* else `cwd`. Never adopts an ancestor `.homegraph` that is not that root
|
|
78
|
+
* (Spec 0061 — fixes parent-harness index hijack).
|
|
79
|
+
*
|
|
80
|
+
* `cwd` is injectable for tests; production callers omit it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveCliMutatingProjectRoot(pathArg?: string | null, cwd?: string): string;
|
|
66
83
|
export declare function findNearestHomeGraphRoot(startPath: string, floor?: string): string | null;
|
|
67
84
|
/**
|
|
68
85
|
* Indexed sub-project roots beneath `root` (bounded breadth-first scan). For
|
package/dist/directory.js
CHANGED
|
@@ -44,6 +44,8 @@ exports.isHomeGraphDataDir = isHomeGraphDataDir;
|
|
|
44
44
|
exports.getHomeGraphDir = getHomeGraphDir;
|
|
45
45
|
exports.isInitialized = isInitialized;
|
|
46
46
|
exports.unsafeIndexRootReason = unsafeIndexRootReason;
|
|
47
|
+
exports.findNearestHarmonyProjectRoot = findNearestHarmonyProjectRoot;
|
|
48
|
+
exports.resolveCliMutatingProjectRoot = resolveCliMutatingProjectRoot;
|
|
47
49
|
exports.findNearestHomeGraphRoot = findNearestHomeGraphRoot;
|
|
48
50
|
exports.findIndexedSubprojectRoots = findIndexedSubprojectRoots;
|
|
49
51
|
exports.hasStructuralKeyword = hasStructuralKeyword;
|
|
@@ -183,6 +185,44 @@ function unsafeIndexRootReason(projectRoot) {
|
|
|
183
185
|
}
|
|
184
186
|
return null;
|
|
185
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Walk up from `startPath` looking for a HarmonyOS project root
|
|
190
|
+
* (`build-profile.json5`). Returns null if none is found before the
|
|
191
|
+
* filesystem root. Spec 0061.
|
|
192
|
+
*/
|
|
193
|
+
function findNearestHarmonyProjectRoot(startPath) {
|
|
194
|
+
let current = path.resolve(startPath);
|
|
195
|
+
const fsRoot = path.parse(current).root;
|
|
196
|
+
while (true) {
|
|
197
|
+
if (fs.existsSync(path.join(current, 'build-profile.json5'))) {
|
|
198
|
+
return current;
|
|
199
|
+
}
|
|
200
|
+
if (current === fsRoot)
|
|
201
|
+
break;
|
|
202
|
+
const parent = path.dirname(current);
|
|
203
|
+
if (parent === current)
|
|
204
|
+
break;
|
|
205
|
+
current = parent;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve the project root for CLI mutating commands (`init` / `index` / `sync`).
|
|
211
|
+
*
|
|
212
|
+
* - Explicit `pathArg` → `path.resolve(cwd, pathArg)` literally (no walk-up).
|
|
213
|
+
* - Omitted / empty → nearest ancestor (or cwd) with `build-profile.json5`,
|
|
214
|
+
* else `cwd`. Never adopts an ancestor `.homegraph` that is not that root
|
|
215
|
+
* (Spec 0061 — fixes parent-harness index hijack).
|
|
216
|
+
*
|
|
217
|
+
* `cwd` is injectable for tests; production callers omit it.
|
|
218
|
+
*/
|
|
219
|
+
function resolveCliMutatingProjectRoot(pathArg, cwd = process.cwd()) {
|
|
220
|
+
const base = path.resolve(cwd);
|
|
221
|
+
if (pathArg !== undefined && pathArg !== null && String(pathArg).trim() !== '') {
|
|
222
|
+
return path.resolve(base, String(pathArg).trim());
|
|
223
|
+
}
|
|
224
|
+
return findNearestHarmonyProjectRoot(base) ?? base;
|
|
225
|
+
}
|
|
186
226
|
function findNearestHomeGraphRoot(startPath, floor) {
|
|
187
227
|
// A floor (an MCP host's explicit --path / rootUri) bounds the walk: an
|
|
188
228
|
// ancestor .homegraph above it must not be adopted — a stray ancestor index
|
|
@@ -386,6 +386,13 @@ function expandGrammarLanguages(languages) {
|
|
|
386
386
|
if (languages.some((l) => l === 'cfml')) {
|
|
387
387
|
languages = [...languages, 'cfscript', 'cfquery'];
|
|
388
388
|
}
|
|
389
|
+
// ArkTS has no WASM of its own. After a modular AA batch, packageless /
|
|
390
|
+
// forgotten-HAR .ets that never entered a PROJECT module fall through to
|
|
391
|
+
// TreeSitterExtractor(..., 'typescript') as a shallow symbol fallback — so
|
|
392
|
+
// the TS grammar must be loaded even when the index set is pure .ets.
|
|
393
|
+
if (languages.some((l) => l === 'arkts')) {
|
|
394
|
+
languages = [...languages, 'typescript'];
|
|
395
|
+
}
|
|
389
396
|
return languages;
|
|
390
397
|
}
|
|
391
398
|
/**
|
|
@@ -196,6 +196,11 @@ export declare class ExtractionOrchestrator {
|
|
|
196
196
|
indexAll(onProgress?: (progress: IndexProgress) => void, signal?: AbortSignal, verbose?: boolean, walBackpressure?: () => Promise<void> | null, storeWriterOpts?: {
|
|
197
197
|
dbPath: string;
|
|
198
198
|
fastInit: boolean;
|
|
199
|
+
/** Spec 0065: close main DB before worker open; reclaim after worker close. */
|
|
200
|
+
exclusiveHandoff?: {
|
|
201
|
+
release: () => void;
|
|
202
|
+
reclaim: () => void;
|
|
203
|
+
};
|
|
199
204
|
} | null): Promise<IndexResult>;
|
|
200
205
|
/**
|
|
201
206
|
* Index specific files
|
package/dist/extraction/index.js
CHANGED
|
@@ -57,6 +57,7 @@ const child_process_1 = require("child_process");
|
|
|
57
57
|
const tree_sitter_1 = require("./tree-sitter");
|
|
58
58
|
const parse_pool_1 = require("./parse-pool");
|
|
59
59
|
const store_writer_1 = require("./store-writer");
|
|
60
|
+
const store_flush_guard_1 = require("./store-flush-guard");
|
|
60
61
|
const cooperative_yield_1 = require("../resolution/cooperative-yield");
|
|
61
62
|
const grammars_1 = require("./grammars");
|
|
62
63
|
const project_config_1 = require("../project-config");
|
|
@@ -1464,6 +1465,7 @@ class ExtractionOrchestrator {
|
|
|
1464
1465
|
// advances (buffered items hold whole file contents, so this bound is
|
|
1465
1466
|
// load-bearing for memory).
|
|
1466
1467
|
while (nextSeq - nextToStore >= windowSize) {
|
|
1468
|
+
(0, store_flush_guard_1.throwIfStoreFlushFailed)(aborted, flushError);
|
|
1467
1469
|
if (inFlight.size > 0)
|
|
1468
1470
|
await Promise.race(inFlight);
|
|
1469
1471
|
else
|
|
@@ -1505,16 +1507,43 @@ class ExtractionOrchestrator {
|
|
|
1505
1507
|
});
|
|
1506
1508
|
windowSize = Math.max(4, pool.size * 2);
|
|
1507
1509
|
log(`Parse worker pool: ${poolSize} worker(s) (remaining files≈${remainingToParse})`);
|
|
1510
|
+
// ArkTS extraction is in-process (requiresInProcessExtraction). Orphan
|
|
1511
|
+
// .ets after the AA batch use TreeSitterExtractor('typescript') on this
|
|
1512
|
+
// thread — workers loading TS wasm does not help the main isolate.
|
|
1513
|
+
if (neededLanguages.includes('arkts')) {
|
|
1514
|
+
await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
|
|
1515
|
+
}
|
|
1508
1516
|
}
|
|
1509
1517
|
else {
|
|
1510
1518
|
await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
|
|
1511
1519
|
}
|
|
1512
1520
|
const storeWorkerPath = path.join(__dirname, 'store-worker.js');
|
|
1521
|
+
/** Spec 0065: true after exclusiveHandoff.release until matching reclaim. */
|
|
1522
|
+
let storeHandoffReleased = false;
|
|
1523
|
+
const reclaimStoreHandoff = () => {
|
|
1524
|
+
if (!storeHandoffReleased)
|
|
1525
|
+
return;
|
|
1526
|
+
storeHandoffReleased = false;
|
|
1527
|
+
storeWriterOpts?.exclusiveHandoff?.reclaim();
|
|
1528
|
+
};
|
|
1513
1529
|
if (storeWriterOpts &&
|
|
1514
1530
|
process.env.HOMEGRAPH_NO_STORE_WORKER !== '1' &&
|
|
1515
1531
|
fs.existsSync(storeWorkerPath)) {
|
|
1516
|
-
|
|
1517
|
-
|
|
1532
|
+
// Phase A (ArkTS / main-thread writes) is done; close the main connection
|
|
1533
|
+
// before the worker opens its own — dual MEMORY/DELETE connections are
|
|
1534
|
+
// what turn a transient SQLITE_BUSY into a hung index (Spec 0065).
|
|
1535
|
+
if (storeWriterOpts.exclusiveHandoff) {
|
|
1536
|
+
storeWriterOpts.exclusiveHandoff.release();
|
|
1537
|
+
storeHandoffReleased = true;
|
|
1538
|
+
}
|
|
1539
|
+
try {
|
|
1540
|
+
storeWriter = new store_writer_1.StoreWriter(storeWorkerPath, storeWriterOpts.dbPath, storeWriterOpts.fastInit);
|
|
1541
|
+
log('Store writer thread active');
|
|
1542
|
+
}
|
|
1543
|
+
catch (err) {
|
|
1544
|
+
reclaimStoreHandoff();
|
|
1545
|
+
throw err;
|
|
1546
|
+
}
|
|
1518
1547
|
}
|
|
1519
1548
|
// Parsing progress only after ArkTS batch (avoids false "Parsing - done").
|
|
1520
1549
|
onProgress?.({
|
|
@@ -1523,135 +1552,152 @@ class ExtractionOrchestrator {
|
|
|
1523
1552
|
total,
|
|
1524
1553
|
});
|
|
1525
1554
|
await new Promise((resolve) => setImmediate(resolve));
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
aborted = true;
|
|
1529
|
-
break;
|
|
1530
|
-
}
|
|
1531
|
-
const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
|
|
1532
|
-
// Read files in parallel (with path validation before any I/O)
|
|
1533
|
-
const fileContents = await Promise.all(batch.map(async (fp) => {
|
|
1534
|
-
if ((0, arkts_1.isArkTSBatchPersisted)(fp)) {
|
|
1535
|
-
return {
|
|
1536
|
-
filePath: fp,
|
|
1537
|
-
content: null,
|
|
1538
|
-
stats: null,
|
|
1539
|
-
error: null,
|
|
1540
|
-
arktsBatchSkipped: true,
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
try {
|
|
1544
|
-
// Indexing read: follow in-root symlinks the directory walk already
|
|
1545
|
-
// descended into (the `../` guard still applies) so files reached
|
|
1546
|
-
// via an in-root symlink-to-outside still index (#935).
|
|
1547
|
-
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, fp, { allowSymlinkEscape: true });
|
|
1548
|
-
if (!fullPath) {
|
|
1549
|
-
(0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
|
|
1550
|
-
return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
|
|
1551
|
-
}
|
|
1552
|
-
const content = await fsp.readFile(fullPath, 'utf-8');
|
|
1553
|
-
const stats = await fsp.stat(fullPath);
|
|
1554
|
-
return { filePath: fp, content, stats, error: null };
|
|
1555
|
-
}
|
|
1556
|
-
catch (err) {
|
|
1557
|
-
return { filePath: fp, content: null, stats: null, error: err };
|
|
1558
|
-
}
|
|
1559
|
-
}));
|
|
1560
|
-
// Dispatch each readable file into the bounded parse window; the window
|
|
1561
|
-
// stores results on the main thread as they arrive.
|
|
1562
|
-
for (const fileEntry of fileContents) {
|
|
1563
|
-
const { filePath, content, stats, error } = fileEntry;
|
|
1564
|
-
const arktsBatchSkipped = 'arktsBatchSkipped' in fileEntry && fileEntry.arktsBatchSkipped;
|
|
1555
|
+
try {
|
|
1556
|
+
for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
|
|
1565
1557
|
if (signal?.aborted) {
|
|
1566
1558
|
aborted = true;
|
|
1567
1559
|
break;
|
|
1568
1560
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1561
|
+
const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
|
|
1562
|
+
// Read files in parallel (with path validation before any I/O)
|
|
1563
|
+
const fileContents = await Promise.all(batch.map(async (fp) => {
|
|
1564
|
+
if ((0, arkts_1.isArkTSBatchPersisted)(fp)) {
|
|
1565
|
+
return {
|
|
1566
|
+
filePath: fp,
|
|
1567
|
+
content: null,
|
|
1568
|
+
stats: null,
|
|
1569
|
+
error: null,
|
|
1570
|
+
arktsBatchSkipped: true,
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
try {
|
|
1574
|
+
// Indexing read: follow in-root symlinks the directory walk already
|
|
1575
|
+
// descended into (the `../` guard still applies) so files reached
|
|
1576
|
+
// via an in-root symlink-to-outside still index (#935).
|
|
1577
|
+
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, fp, { allowSymlinkEscape: true });
|
|
1578
|
+
if (!fullPath) {
|
|
1579
|
+
(0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
|
|
1580
|
+
return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
|
|
1581
|
+
}
|
|
1582
|
+
const content = await fsp.readFile(fullPath, 'utf-8');
|
|
1583
|
+
const stats = await fsp.stat(fullPath);
|
|
1584
|
+
return { filePath: fp, content, stats, error: null };
|
|
1585
|
+
}
|
|
1586
|
+
catch (err) {
|
|
1587
|
+
return { filePath: fp, content: null, stats: null, error: err };
|
|
1588
|
+
}
|
|
1589
|
+
}));
|
|
1590
|
+
// Dispatch each readable file into the bounded parse window; the window
|
|
1591
|
+
// stores results on the main thread as they arrive.
|
|
1592
|
+
for (const fileEntry of fileContents) {
|
|
1593
|
+
const { filePath, content, stats, error } = fileEntry;
|
|
1594
|
+
const arktsBatchSkipped = 'arktsBatchSkipped' in fileEntry && fileEntry.arktsBatchSkipped;
|
|
1595
|
+
if (signal?.aborted) {
|
|
1596
|
+
aborted = true;
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1599
|
+
if ((0, arkts_1.isArkTSBatchPersisted)(filePath) || arktsBatchSkipped) {
|
|
1600
|
+
processed++;
|
|
1601
|
+
filesIndexed++;
|
|
1602
|
+
onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
if (!(0, context_1.isArktsBatchRunning)()) {
|
|
1606
|
+
onProgress?.({
|
|
1607
|
+
phase: 'parsing',
|
|
1608
|
+
current: processed,
|
|
1609
|
+
total,
|
|
1610
|
+
currentFile: filePath,
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
if (error || content === null || stats === null) {
|
|
1614
|
+
processed++;
|
|
1615
|
+
filesErrored++;
|
|
1616
|
+
errors.push({
|
|
1617
|
+
message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
1618
|
+
filePath,
|
|
1619
|
+
severity: 'error',
|
|
1620
|
+
code: 'read_error',
|
|
1621
|
+
});
|
|
1622
|
+
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
// Honour MAX_FILE_SIZE. Without this check, vendored generated
|
|
1626
|
+
// headers, minified bundles, and other multi-MB files get indexed,
|
|
1627
|
+
// wasting WASM heap and the worker recycle budget on inputs with no
|
|
1628
|
+
// useful symbols. The single-file extractFile path already enforces
|
|
1629
|
+
// this; the bulk path used to silently skip the check.
|
|
1630
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
1631
|
+
processed++;
|
|
1632
|
+
filesSkipped++;
|
|
1633
|
+
errors.push({
|
|
1634
|
+
message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
|
|
1635
|
+
filePath,
|
|
1636
|
+
severity: 'warning',
|
|
1637
|
+
code: 'size_exceeded',
|
|
1638
|
+
});
|
|
1639
|
+
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
// Parse on the pool (main thread stays unblocked). Errors/timeouts are
|
|
1643
|
+
// handled inside feed() → recordParseFailure, feeding the retry pass.
|
|
1644
|
+
await feed(filePath, content, stats);
|
|
1582
1645
|
}
|
|
1583
|
-
if (
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1646
|
+
if (aborted)
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
// Drain parses still in flight (skip on abort — we tear down below instead),
|
|
1650
|
+
// then commit any results the cursor hasn't reached yet.
|
|
1651
|
+
if (!aborted) {
|
|
1652
|
+
await Promise.all(inFlight);
|
|
1653
|
+
await flushOrdered();
|
|
1654
|
+
if (flushError) {
|
|
1655
|
+
if (storeWriter) {
|
|
1656
|
+
await storeWriter.close();
|
|
1657
|
+
storeWriter = null;
|
|
1658
|
+
}
|
|
1659
|
+
throw flushError;
|
|
1594
1660
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
errors.push({
|
|
1604
|
-
message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
|
|
1605
|
-
filePath,
|
|
1606
|
-
severity: 'warning',
|
|
1607
|
-
code: 'size_exceeded',
|
|
1608
|
-
});
|
|
1609
|
-
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1610
|
-
continue;
|
|
1661
|
+
if (storeWriter) {
|
|
1662
|
+
try {
|
|
1663
|
+
await storeWriter.drain();
|
|
1664
|
+
}
|
|
1665
|
+
finally {
|
|
1666
|
+
await storeWriter.close();
|
|
1667
|
+
storeWriter = null;
|
|
1668
|
+
}
|
|
1611
1669
|
}
|
|
1612
|
-
// Parse on the pool (main thread stays unblocked). Errors/timeouts are
|
|
1613
|
-
// handled inside feed() → recordParseFailure, feeding the retry pass.
|
|
1614
|
-
await feed(filePath, content, stats);
|
|
1615
1670
|
}
|
|
1616
|
-
if (aborted)
|
|
1617
|
-
|
|
1618
|
-
}
|
|
1619
|
-
// Drain parses still in flight (skip on abort — we tear down below instead),
|
|
1620
|
-
// then commit any results the cursor hasn't reached yet.
|
|
1621
|
-
if (!aborted) {
|
|
1622
|
-
await Promise.all(inFlight);
|
|
1623
|
-
await flushOrdered();
|
|
1624
|
-
if (flushError) {
|
|
1625
|
-
if (storeWriter)
|
|
1671
|
+
if (signal?.aborted || aborted) {
|
|
1672
|
+
if (storeWriter) {
|
|
1626
1673
|
await storeWriter.close();
|
|
1627
|
-
|
|
1674
|
+
storeWriter = null;
|
|
1675
|
+
}
|
|
1676
|
+
if (pool)
|
|
1677
|
+
await pool.destroy();
|
|
1678
|
+
return {
|
|
1679
|
+
success: false,
|
|
1680
|
+
filesIndexed,
|
|
1681
|
+
filesSkipped,
|
|
1682
|
+
filesErrored,
|
|
1683
|
+
filesDiscovered: total,
|
|
1684
|
+
nodesCreated: totalNodes,
|
|
1685
|
+
edgesCreated: totalEdges,
|
|
1686
|
+
errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
|
|
1687
|
+
durationMs: Date.now() - startTime,
|
|
1688
|
+
};
|
|
1628
1689
|
}
|
|
1690
|
+
}
|
|
1691
|
+
finally {
|
|
1692
|
+
// Reclaim before retry / endBulk* — those paths need the main connection.
|
|
1629
1693
|
if (storeWriter) {
|
|
1630
1694
|
try {
|
|
1631
|
-
await storeWriter.drain();
|
|
1632
|
-
}
|
|
1633
|
-
finally {
|
|
1634
1695
|
await storeWriter.close();
|
|
1635
|
-
storeWriter = null;
|
|
1636
1696
|
}
|
|
1697
|
+
catch { /* best-effort */ }
|
|
1698
|
+
storeWriter = null;
|
|
1637
1699
|
}
|
|
1638
|
-
|
|
1639
|
-
if (signal?.aborted || aborted) {
|
|
1640
|
-
if (storeWriter)
|
|
1641
|
-
await storeWriter.close();
|
|
1642
|
-
if (pool)
|
|
1643
|
-
await pool.destroy();
|
|
1644
|
-
return {
|
|
1645
|
-
success: false,
|
|
1646
|
-
filesIndexed,
|
|
1647
|
-
filesSkipped,
|
|
1648
|
-
filesErrored,
|
|
1649
|
-
filesDiscovered: total,
|
|
1650
|
-
nodesCreated: totalNodes,
|
|
1651
|
-
edgesCreated: totalEdges,
|
|
1652
|
-
errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
|
|
1653
|
-
durationMs: Date.now() - startTime,
|
|
1654
|
-
};
|
|
1700
|
+
reclaimStoreHandoff();
|
|
1655
1701
|
}
|
|
1656
1702
|
// Report 100% so the progress bar doesn't hang at 99%
|
|
1657
1703
|
onProgress?.({
|