feat(extraction): add ArkTS language support with ArkUI dispatch bridges (#396, #512, #890 via #648) (#1186)

Adds ArkTS (.ets, HarmonyOS/OpenHarmony) as a first-class language:
full TypeScript-grade extraction via the harmony-contrib tree-sitter
grammar (MIT, vendored byte-identical from the tree-sitter-arkts 0.2.0
npm tarball), plus the ArkUI constructs that make HarmonyOS apps
traceable:

- @Component/@ComponentV2 structs with decorators from both grammar
  positions; members extract as class members with qualified names.
- build() component trees: child instantiation edges via
  arkui_component_expression, no synthesizer needed.
- Attribute chains emitted dot-prefixed and resolved ONLY against
  @Extend/@Styles/@AnimatableExtend/@Builder helpers (unique-or-drop) —
  bare-name fallthrough produced 36,840 wrong edges (17% of calls) on
  the OpenHarmony samples monorepo. All four grammar chain shapes
  handled, including the detached-chain forms.
- .onClick(this.handler) method-reference bindings.
- ohpm workspace modules: bare imports follow oh-package.json5 file:
  deps (ambiguous names dropped), honoring each module's main entry —
  which also lets .ts consumers resolve .ets modules.
- ArkUI dynamic-dispatch bridges, all provenance:'heuristic' with
  wiring-site metadata: assignment-gated state->build() re-render
  (V1 @State family + V2 @Local/@Provider/@Consumer),
  @ohos.events.emitter emit->subscriber pairing on static event keys
  (numeric ids same-file, named constants same-module, fan-out capped),
  and router.pushUrl literal urls -> the target page's @Entry struct.
- $r/$rawfile resource intrinsics treated as built-ins; arkts joins the
  web language family, value-reference edges, re-export chase, and the
  other TS-applicable gates.

Also ships a language-agnostic index-completeness guard: indexAll
stamps index_state (indexing -> complete/partial/failed), reconciles
discovered vs accounted files (a loaded run silently dropped 37 files),
and codegraph status surfaces truncated/partial indexes in human and
--json output.

Validated on HarmoneyOpenEye (82 files), CoolMallArkTS (528, modular
ohpm + ArkUI V2), and openharmony/applications_app_samples (11,693
files, 202,890 nodes stable across re-index, attribute false-positive
audit 36,840 -> 588 residual all-plausible). Supersedes PRs #656 and
#988 with credit — both informed this implementation.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-06 09:07:15 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent f8cdbe3c67
commit 99152212a9
21 changed files with 1695 additions and 21 deletions
+52
View File
@@ -437,6 +437,11 @@ export class CodeGraph {
}
try {
const before = this.queries.getNodeAndEdgeCount();
// Mark the index as in-flight BEFORE any writes: a run killed
// mid-index (OOM, SIGKILL, the #850 liveness watchdog) leaves this
// marker behind, so `codegraph status` can tell a truncated index
// from a completed one instead of silently serving partial results.
try { this.queries.setMetadata('index_state', 'indexing'); } catch { /* metadata is advisory */ }
// Segment vocabulary starts empty and is repopulated by the node write
// path as every file (re-)indexes below — so a full index is also the
// orphan-cleanup pass for names deleted since the last one.
@@ -513,6 +518,37 @@ export class CodeGraph {
} catch { /* metadata is advisory — never fail an index over it */ }
}
// Reconcile the scan's ground truth against what the pipeline
// accounted for. A shortfall means files were silently dropped
// (observed in the wild: a run under heavy load came up 37 files
// short with no error) — record it and tell the user, don't let the
// index pass as complete.
try {
if (!result.success) {
this.queries.setMetadata('index_state', 'failed');
} else {
const accounted = result.filesIndexed + result.filesSkipped + result.filesErrored;
const discovered = result.filesDiscovered;
const shortfall = discovered !== undefined ? discovered - accounted : 0;
if (discovered !== undefined && shortfall > 0) {
this.queries.setMetadata('index_state', 'partial');
this.queries.setMetadata('index_files_discovered', String(discovered));
this.queries.setMetadata('index_files_accounted', String(accounted));
result.errors.push({
message: `Index is missing ${shortfall} of ${discovered} discovered files (indexed ${result.filesIndexed}, skipped ${result.filesSkipped}, errored ${result.filesErrored}). The index is PARTIAL — re-run \`codegraph index\`.`,
severity: 'warning',
code: 'index_partial',
});
} else {
this.queries.setMetadata('index_state', 'complete');
if (discovered !== undefined) {
this.queries.setMetadata('index_files_discovered', String(discovered));
this.queries.setMetadata('index_files_accounted', String(accounted));
}
}
}
} catch { /* metadata is advisory — never fail an index over it */ }
return result;
} finally {
this.fileLock.release();
@@ -761,6 +797,22 @@ export class CodeGraph {
return this.queries.getLastIndexedAt();
}
/**
* Completeness of the last full index run. `'complete'` is the only good
* state. `'indexing'` after the fact means a run was killed mid-index (OOM,
* SIGKILL, liveness watchdog) and the on-disk index is truncated;
* `'partial'` means the run finished but silently dropped files
* (discovered > indexed+skipped+errored); `'failed'` means it reported
* failure. `null` = index predates this marker. Surfaced by
* `codegraph status`.
*/
getIndexState(): 'indexing' | 'complete' | 'partial' | 'failed' | null {
const raw = this.queries.getMetadata('index_state');
return raw === 'indexing' || raw === 'complete' || raw === 'partial' || raw === 'failed'
? raw
: null;
}
/**
* Which engine built the current index: the package version + extraction
* version stamped at the last full `indexAll`. Either field is null for an