Commit Graph
651 Commits
Author SHA1 Message Date
99152212a9 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>
2026-07-06 09:07:15 -05:00
f8cdbe3c67 feat(terraform): remote-state bridge, provider aliases, moved/import/check refs (#1174)
Follow-ups noted in #1173:

- cloudposse/atmos remote-state: module.M.outputs.X emits a scoped
  module.M:remote-output.X candidate; the resolver bridges it to the
  target COMPONENT's own output when every gate holds — the module
  source is the stack-config remote-state module, the component name is
  static (a literal, or component = var.X whose variable declares a
  literal default in the same directory), and exactly one directory in
  the repo matches the component name and declares that output. Dynamic
  (each.value) or ambiguous wiring stays unlinked. On
  cloudposse/terraform-aws-components: 254 remote-state bridge edges,
  every one re-derived from a matching source declaration (789/789
  cross-directory output edges explained: 528 local-module + 254
  remote-state + 7 checker-artifact false alarms under deprecated/);
  coverage 66.4% -> 69.1%.

- provider aliases: provider "aws" { alias = "east" } is addressed as
  provider.aws.east so aliased and default configurations stop
  colliding; provider = aws.east on a resource/data block (and the
  values of a module's providers map) reference the selected
  configuration, resolved same-directory first then up the module tree
  — the one construct Terraform genuinely inherits from parents. The
  selection is no longer misread as a resource reference (aws.east).

- moved/import/removed blocks reference the resource addresses they
  name (anchored to the file node — no phantom symbols), so a
  refactor's paper trail joins the graph; check-assert conditions
  contribute their references while check-scoped data blocks keep
  indexing as before. Scoped module candidates are suppressed there:
  module.a.aws_x.b names a resource inside a module instance, not an
  output. +91 edges on cloud-foundation-fabric's moved-heavy stages.

Also fixes a latent test bug from #1173: cg.getNodeById is not public
API (cg.getNode is) — it only passed because the asserted edge list was
empty.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:38:03 -05:00
6c24f4bddf feat(extraction): add Terraform/OpenTofu language support with module-boundary bridging (#83, #310, #648 — carries #706) (#1173)
* feat(extraction): add Terraform and OpenTofu language support

Index .tf, .tfvars, and .tofu files via the tree-sitter-terraform dialect
of HCL (vendored from @tree-sitter-grammars/tree-sitter-hcl, Apache-2.0).

Symbols extracted:
- resource / data  → class  (qualified "type.name" / "data.type.name")
- module           → module (qualified "module.name")
- variable         → variable (qualified "var.name")
- output           → variable (qualified "output.name")
- provider         → namespace
- locals           → constant per attribute (qualified "local.key")

References resolved cross-file:
- var.X, local.X, module.M[.out], data.T.N[.attr], <type>.<name>[.attr]
- built-ins skipped: each.*, count.*, self.*, path.*, terraform.workspace

The Terraform framework resolver disambiguates same-named candidates
across modules by preferring the one in the same directory as the
reference site, then by closest common-ancestor path, falling back to
the generic name matcher only when neither applies.

Validated on two Terraform monorepos (277 and 470 .tf files): indexing
runs in 1.3s and 2.4s respectively, query latency stays under 200ms,
and cross-module references resolve to the correct module 100% of the
time on inspected samples.

18 new extraction tests; full suite 1146/1148 green (2 pre-existing
flaky skips, 0 regressions).

* feat(terraform): bridge the module boundary and enforce directory scoping

Builds on #706. The module declaration was a dead end: module.M.out
resolved to the declaration and stopped, module inputs never reached the
child module's variables, and impact could not cross the boundary — on
real multi-module repos that breaks the core blast-radius question
("what breaks upstream if I change this module's variable/output").

- module blocks now wire across the boundary through :-scoped refs only
  the Terraform resolver understands: module.M:var.<input> → the child's
  variable node, module.M:output.<o> → the child's output node (emitted
  alongside the module.M declaration ref), and module.M:file → the local
  source directory's entry file (imports). Registry/git sources emit no
  file ref and resolve nothing — an out-of-repo module stays a visible
  boundary instead of a guess.
- .tfvars top-level assignments reference the variable they set, walking
  up to the nearest ancestor directory (envs/prod.tfvars → root vars).
- Resolution now enforces Terraform's real scoping: same-directory only
  (no cross-module fallback by common path prefix, no single-candidate
  anywhere-in-tree binding), and terraform refs never fall through to
  the generic name matcher — var.X can never legally bind outside its
  module directory, so the fallback could only add wrong edges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(terraform): README language table + changelog entry + agent-eval corpus

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Javier Rodríguez Fernández <jfernandez@freepik.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:37:07 -05:00
e1a8d888e5 feat(extraction): add CUDA language support (.cu/.cuh) (#387, #648) (#1172)
CUDA rides the C++ grammar via the Metal (#1121) dialect pattern:
blankCudaConstructs (offset-preserving) blanks execution-space specifiers
(__global__ family), __launch_bounds__(...), and <<<grid, block>>> launch
configs — which otherwise lex as shift operators and destroy the
host→kernel call edge entirely. Gated by .cu/.cuh extension OR by content
(looksLikeCudaSource), because much real CUDA lives in .h/.hpp headers:
cutlass launches most kernels from headers and flash-attention's launch
templates are .h. Safe by construction — no CUDA marker is valid C++
anywhere, and the launch blank is bounded + brace-balance-checked so a
stray <<< (committed merge-conflict markers) can never blank real code.

All real-world launch styles connect: plain, templated
(k<T, 256><<<...>>>), function-pointer (auto kernel = &fn<...>; with
branch reassignments each linked), dim3{...} brace-init configs, and
kernels defined through name-in-first-argument macros
(DEFINE_FLASH_FORWARD_KERNEL style — gtest TEST_F / PYBIND11_MODULE
shapes deliberately excluded by the two-lone-identifiers rule).

Two general C++ resolution wins the flow validation forced out:
- namespace blocks now prefix contained symbols' qualifiedNames
  (prefix-only — no namespace nodes, avoiding #1093-style crowd-out), so
  ns::fn(...) calls resolve; previously every namespace-qualified C++
  call was a permanently dead edge. cutlass: +30,864 edges (~10%), node
  count byte-identical.
- templated callees (fn<T, 256>(args)) strip template args at extraction
  (mirroring #1043 for base classes), so they match their definitions.

Validated on llm.c (165 host→kernel launch edges, was 0),
flash-attention (run_flash_fwd → flash_fwd_kernel → compute_attn traces
in one codegraph_explore call), and NVIDIA CUTLASS; fmt as the plain-C++
control (unchanged). A/B n=2/arm: Read/Grep displacement decisive on all
three repos (flash-attention Reads 29,13 → 5,2).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:41:45 -05:00
1441933a26 feat(extraction): add Solidity language support (.sol) (#374, #648) (#1170)
Contracts/libraries/interfaces, structs, enums, modifiers, events, errors,
state variables; call edges for emit/revert/modifier guards/base-constructor
chains/library calls; is-inheritance with implements reclassification;
import resolution. Validated on solmate, solady, openzeppelin-contracts.

Lands #667.

Co-authored-by: naiba <hi@nai.ba>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:05:03 -05:00
a0208feaac feat(extraction): index Erlang escripts and OTP app resource files (#635, #648) (#1169)
escripts (.escript) index like any module — the ELP grammar has a
first-class shebang node, so no source transform is needed; main/1 and its
helpers get full function/call extraction.

OTP application resource files (<app>.app.src and compiled <app>.app) join
the graph as Erlang terms the grammar parses natively. They route by full
suffix (their last-dot extension, .src, is far too generic for the
extension map). The application tuple yields structure: {mod, {Mod, _}}
links the app to its callback module — the app's entry point — and
{applications, [...]} / {included_applications, [...]} connect umbrella
sibling apps, resolving through the OTP app-name == module-name convention;
kernel/stdlib and other out-of-repo apps stay unresolved.

App-file refs resolve only ever to MODULES: validation on emqx caught the
ssl OTP-app dependency resolving to a test helper FUNCTION named ssl (the
same defect class as the earlier -behaviour gate), so the matchReference
module-only gate now covers every ref an .app/.app.src file emits.

Validated on emqx: 2 app.src + 6 escripts indexed, entry-module and
umbrella-dependency edges all namespace-targeted post-gate, escript
functions extracted; a stray legacy/module.src stays unknown.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:32:59 -05:00
a5b8cd8e25 feat(extraction): Erlang macro-body call linkage (#635, #648) (#1168)
Calls hidden inside -define bodies were invisible: the extractor consumed
pp_define without walking the replacement, and macro use sites produced no
edges, so a call path routed through a macro (ejabberd's SQL upsert macros,
logging wrappers) was completely dark.

The macro's constant node now participates in the graph. The -define body's
calls are attributed to the MACRO — true exactly once, instead of a per-use
duplicate that would explode on logging macros — and each use site links
in: ?MACRO(...) with arguments emits a `calls` ref (inlined code joins the
call chain), a bare ?CONSTANT read emits `references` (answering "where is
this macro used" without polluting call paths). Compiler-predefined macros
(?MODULE, ?LINE, ?FUNCTION_NAME, ...) are excluded, macro-use arguments
keep walking so a call nested in ?assertEqual(ok, do_thing()) still
attributes to the enclosing function, and macro-to-macro chains connect.

Validated: node counts unchanged on cowboy/ejabberd/emqx; edges +26/+7.3K/
+42K with honest hub shapes (?T i18n, ?SLOG logging, ?QOS_1 protocol
constants); 40/40 sampled edges precise; +1.3s index cost on emqx's 2,273
files. The payoff chain on ejabberd: set_password_scram_t → ?SQL_UPSERT_T →
ejabberd_sql:sql_query_t — database writes through SQL macros now trace
end-to-end.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:19:46 -05:00
7e3d44fa96 feat(extraction): Erlang gen_server registered-name dispatch targets (#635, #648) (#1167)
gen_server:call/cast/send_request now connects to the TARGET module's
handle_call/handle_cast for every statically-named target, not just self:
a bare atom reaches the module of that name (OTP's {local, ?MODULE}
convention names a server after its module), and a macro defined as a bare
atom (-define(STORE, kv_store)) resolves the same way, alongside the
existing ?MODULE / -define(SERVER, ?MODULE) self paths. A registered name
that matches no module emits a qualified ref that never resolves — silent,
never guessed. Pid, var, and tuple targets ({global, Name}, {Name, Node})
stay unlinked.

Validated on emqx: 53 new edges, 53/53 precise (each source line is a real
registered-name gen_server request; each target module self-registers under
that name, macro-indirected registrations included). Nearly all are
test-suite → handler links — production code goes through API wrappers the
self path already covers — which is exactly the tests-exercising-this-
handler linkage blast-radius and test-gap reporting consume. ejabberd
yields zero (it always wraps): no false positives invented.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:08:28 -05:00
2217a35943 feat(resolution): Erlang behaviour-callback dispatch synthesizer (#635, #648) (#1166)
Bridges the OTP callback boundary: a framework call through a variable
module — cowboy's Handler:init / Middleware:execute folds, a plugin
manager's Mod:callback(...) — now links to the repo's implementers of the
behaviour declaring that callback, so codegraph_explore connects flows
end-to-end across behaviour dispatch instead of stopping at it.

Precision gates: the callback arity must match the site, exactly one
in-repo behaviour may declare that (name, arity) — a collision bails
(cowboy's init/2 is declared by five handler-flavored behaviours and
correctly stays silent) — the implementer must export the callback, and
above the fan-out cap the site is skipped entirely (ejabberd's gen_mod
with ~230 implementers stays a visibly dynamic boundary). Behaviour
discovery scans -callback declarations in every module so implementer-less
behaviours still gate ambiguity. Edges carry provenance:'heuristic' with
synthesizedBy:'erlang-behaviour' and the wiring site, rendered as dynamic
dispatch in explore.

Validated per the dispatch-family playbook: cowboy 38 edges (middleware
chain, stream-handler folds, sub-protocol upgrade), ejabberd 598, emqx 843;
36/36 sampled edges precise (target declares the via-behaviour and exports
the callback); node counts unchanged; ~1.4s added on emqx's 2,273 files;
zero-control clean. The cowboy request flow connects in one explore call.

Includes an Erlang comment stripper (%-comments, string/atom/$-char aware)
for the dispatch-site scans.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:53:04 -05:00
6511722250 feat(extraction): add Erlang language support (.erl/.hrl) (#635, #648) (#1165)
Vendored WhatsApp/tree-sitter-erlang 0.19 (the ELP grammar, ABI 14) with an
Erlang-shaped extractor: multi-clause/multi-arity functions merged into one
symbol, -spec signatures, records with fields, -type/-opaque aliases, -define
macros, -include/-include_lib file edges, and -export-driven visibility.

Modules wrap in a namespace so remote mod:fn(...) calls resolve through the
existing qualified-name matcher as mod::fn with zero resolver changes.
-behaviour declarations link to the behaviour module — gated to namespace
targets only (bare-name fallthrough linked -behaviour(supervisor) to an
unrelated macro constant on emqx). OTP indirection with static targets is
followed: spawn/apply/proc_lib/timer/rpc MFA-argument callees, and
gen_server:call/cast(?MODULE | ?SERVER) to the module's own
handle_call/handle_cast. Var-module dispatch and message sends stay
deliberately unlinked. codegraph_explore also normalizes Erlang-native query
spelling (mod:fn/3, init/2) so named symbols resolve as typed.

Benchmarked on cowboy (189 files), ejabberd (414), emqx (2,447): extraction
PASS on all three; with-codegraph arms reached 2/2/0 file Reads vs 10/5+/19
without, fastest on the largest repo.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:32:20 -05:00
63e1b5a23a feat(extraction): add Visual Basic .NET language support (.vb) (#648, #639, #170) (#1164)
Vendored patched govindbanura/tree-sitter-vbnet grammar (MIT, ~20-fix patch
+ new external scanner for XML literals and multi-line LINQ continuation;
provenance + rebuild instructions in docs/grammars/tree-sitter-vbnet.md),
vbnet extractor with VB-specific call/index disambiguation, Inherits/
Implements heritage, As New instantiation, events, Declare P/Invoke, and
MustOverride abstract members.

Parse health on five real repos: PolicyPlus 100%, CompactGUI 100%,
staxrip 95.2%, SCrawler 87.2%, PCL 87.5% (upstream grammar: 3-18%).
Retrieval A/B (sonnet): 26-43% faster with 0-5 file reads vs 7-20 without.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 11:55:45 -05:00
d7afc8cc1f docs(grammars): record the sent upstream tree-sitter-cobol PR (#41) (#1162)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:23:23 -05:00
41620c60fa feat(extraction): add COBOL language support (.cbl/.cob/.cpy) (#590, #648) (#1161)
Programs, sections/paragraphs (reconstructed extents over the grammar's
flat header stream), PERFORM/THRU/GO TO/CALL call edges, COPY copybook
imports incl. standalone .cpy fragments, DATA DIVISION records/fields/
88-levels with write-site impact references, and CICS flows: EXEC
LINK/XCTL program targets (literal + same-file VALUE deref), EXEC SQL
INCLUDE, and pseudo-conversational RETURN/START TRANSID hops resolved
to the owning program via a CICS framework resolver. Fixed and free
source format (free format via a scanner wide-mode sentinel).

Grammar: vendored wasm built from a patched yutaro-sakamoto/
tree-sitter-cobol (EXEC blocks as an external-scanner token, copybook
fragment entry point, single-quote continuation, COPY REPLACING
pseudo-text, NOT=, CALL GIVING, ENTRY, FREE, bitwise ops, abbreviated
relations, COBOL-2002 usages, and more). Patch + provenance + upstream
PR draft in docs/grammars/. Parse health: AWS CardDemo 43/44 native
(upstream: 9/31), 44/44 through preParse; copybooks 28/29; CobolCraft
free-format 17/17 (upstream: 0); NIST COBOL85 unchanged at 373/382.

Copybook members resolve to files like C includes (basename index,
name-matcher short-circuit so compiler-supplied members stay honestly
unresolved): CardDemo imports 5 -> 285. Impact proof: ACCT-CURR-BAL
(CVACT01Y copybook) surfaces its 4 writer programs cross-file.

Also: run-all.sh now neutralizes the ambient prompt-hook in both A/B
arms (CODEGRAPH_NO_PROMPT_HOOK=1); COBOL corpus entries for agent-eval.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:17:53 -05:00
7d624ecfac feat(resolution): CFML receiver-type inference for locals, typed args, and component properties (#1155)
CFML joins the #1108 receiver-inference family: new/createObject/typed-arg/property(inject) declarations type the receiver, variables./this. fields scan whole-file, method QNs re-scoped to Class::member in all three extraction paths. 1,649 typed edges on fw1/ColdBox/CFWheels, 1,649/1,649 audit-consistent, inherited methods resolve via #1152 extends edges.

Co-authored-by: ghedwards <125586+ghedwards@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:02:16 -05:00
5f22da35f3 feat(resolution): resolve CFML dotted and relative component-path inheritance (#1152) (#1154)
extends="coldbox.system.web.Controller" (dotted) and extends="../base" (relative) now resolve to the right component via directory-corroborated matching; >=1 corroborating segment required, ties yield no edge. fw1 14->47, ColdBox 21->242, CFWheels 60->201 inheritance edges; 394/394 audited path-consistent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:41:55 -05:00
816bacb7f2 feat(extraction): add CFML language support (.cfc/.cfm/.cfs) (#1118) (#1153)
Tag-based and bare-script CFML, extends/implements, <cfscript>/<cfquery> delegation, BOM + unquoted-attribute handling. Wasm grammars verified bit-for-bit reproducible from cfmleditor/tree-sitter-cfml. Validated on FW/1, ColdBox, CFWheels. Follow-up: #1152.

Co-authored-by: ghedwards <125586+ghedwards@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:25:53 -05:00
cc89146454 feat(extraction): index Metal shader files (.metal) via the C++ grammar (#1121) (#1151)
.metal was absent from EXTENSION_MAP, so Metal Shading Language files were
silently skipped. MSL ≈ C++14, and the C++ grammar extracts its functions,
structs, type aliases, and call edges at parity with plain C++ — except MSL's
post-declarator [[attribute]] annotations, which misparse struct fields into
spurious extends refs from the struct to the field's own type (a wrong
inheritance edge whenever the repo typedefs float3/float4x4 itself, common in
shared ShaderTypes.h). blankMetalAttributes blanks them pre-parse,
offset-preserving, following the blankCppExportMacros pattern (#1061), gated
to .metal files only — in regular C++ the attribute position is legal syntax
the grammar parses natively. The preParse hook gains an optional filePath
param to support the gate.

Validated on llama.cpp's ggml-metal.metal (10.7k lines: 130 kernels vs 113
`kernel void` ground-truth lines, rope_yarn resolves its 4 kernel callers)
and SDL's shaders (PQtoLinear ← GetOutputColor), 0 bogus extends edges.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:51:41 -05:00
35611b92bb fix(prompt-hook): close the segment-vocab integrity gaps (#1141, #1142, #1144, #1145, #1146) (#1150)
Five hardening fixes to the #1136 MEDIUM (graph-derived) tier:

- #1141: updateNode() now writes the segment vocabulary like insertNode()
  does — framework post-extract renames (NestJS route prefixing) left the
  new name permanently unsearchable (the old rows orphaned, the backfill
  gated on an EMPTY vocab, so even a full re-index re-created the drift).
- #1142: new CodeGraph.healSegmentVocabIfEmpty() — the hook opens the
  graph without sync, so a database migrated from pre-vocab schema kept
  the MEDIUM tier dormant until some unrelated sync ran. The hook heals
  on first use (one SELECT when populated; lock-aware, defers to a
  running sync) and records noop-vocab-empty when it can't.
- #1144: a name whose only nodes are file/import kind is skipped instead
  of falling back to surfacing an import statement as a matched symbol;
  import specifiers no longer enter the vocab at all (shared
  isSegmentableKind gate across insertNode/updateNode/rebuild page query)
  since they can never be surfaced and only inflate rarity statistics.
- #1145: plural variant folding is keyed on English plural spelling —
  bare-s plurals no longer mint a bogus -es sibling (services→servic),
  unambiguous sibilant-es plurals no longer mint a bogus -s sibling
  (classes→classe), trailing -ss singulars no longer strip (class→clas);
  genuinely ambiguous endings (caches/databases) still emit both keys.
- #1146: getSegmentCoOccurrence folds variants to their original word
  inside the SQL (CASE mapping + COUNT(DISTINCT word)) so a plural pair
  of ONE word can't tie with a genuine two-word match and crowd it past
  the pre-fold ORDER BY/LIMIT; the JS re-check stays as the honesty layer.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:23:54 -05:00
be55b93d02 fix(prompt-hook): record high-tier gate telemetry only when context was actually injected (#1143) (#1149)
gate('high-keyword'/'high-token') sat outside the injection guard, so an
errored or empty codegraph_explore still counted as a HIGH-tier success.
The gate telemetry is the measured recall/precision funnel that decides
whether the tiered gate design survives — a delivery failure must degrade
it toward noop-*, not inflate the high tiers. Failures now record
noop-explore-keyword / noop-explore-token. Doc enum updated (including
the noop-vocab-empty outcome the #1142 fix adds next).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:13:02 -05:00
2f70eb3d32 fix(sync,installer): time-bound the git/npm subprocess calls that had no timeout (#1139) (#1148)
extraction/index.ts bounds every git call it makes; worktree.ts,
git-hooks.ts, and the installer's npm install -g did not, so a stuck
subprocess blocked the caller indefinitely. Worst case was the daemon:
gitWorktreeRoot/gitCommonDir run (memoized) on the main event loop while
serving MCP clients, where an unbounded git hang would trip the 60s
liveness watchdog and SIGKILL a healthy daemon. git calls get 5s, the
interactive npm install 120s. Regression tests assert the option through
a mocked child_process plus a per-file call-site sweep.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:11:19 -05:00
713ab7af43 fix(prompt-hook): bound the call/trace/affect/connect stems on the right so ordinary words can't fire the gate (#1138) (#1147)
The multilingual structural-question gate (#1134) matches stems as open
prefixes (left boundary only) so derived forms fire without enumeration.
Four English stems have common non-structural completions — callus,
calligraphy, Connecticut, connective, affectionate, Tracey — that
false-fired the HIGH (full-explore) tier. Those four now enumerate their
structural suffixes and re-assert the right boundary; callbacks/callable/
call sites are included so no structural form regresses. Also documents
the verified-unfixable Korean homograph class on the unsegmented table
(#1140): segmentation can't split 구조대 from 구조가, and a denylist would
break 구조대로.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:05:51 -05:00
81cb59a86e fix(resolution): yield per ref and cache hot per-ref work so the watchdog can't kill a valid index (#1122) (#1137)
The #850 liveness watchdog was killing valid `codegraph init`/`index` runs
at "Resolving refs 0-2%" on large collision-heavy repos (18-25K-file Java
monorepos on slower hardware). #1105's cooperative yielding assumed a
500-ref sub-chunk is always cheap, but per-ref cost is unbounded: a
colliding method name (`execute`, `process`, ...) whose candidate set
misses the 5,000-entry name LRU re-fetches every same-named row
(unbounded SELECT + materialization, measured 8.8ms at just 4K collisions
on an M4 — linear in collision count), and receiver-type inference
re-split the whole source file per ref (~20% of total index CPU). A dense
pocket multiplied that past the 60s window and the heartbeat starved.

Three guards, no behavior change:
- resolveBatchYielding checkpoints after EVERY ref (maybeYield is a ~ns
  time check when under budget), so a slow pocket can never run more than
  one ref past the yield budget.
- resolveMethodOnType's ref-independent candidate filter is memoized per
  (language, Type::method) on the resolver context; per-ref
  disambiguation (import FQN #314, call-site file #1079) stays outside
  the memo.
- Receiver inference reads lines through a per-file LRU (shared and C++
  inferrers), and skips generated/minified lines >10K chars instead of
  regex-scanning them per ref.

Measured on a 4,028-file synthetic Java bank repo (392K refs): mid-loop
max event-loop stall 1528ms -> 546ms under cache thrash, total init
250.9s -> 96.8s at default config.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:34:56 -05:00
e699ee9686 feat(prompt-hook): graph-derived gate tier + confidence-tiered injection + gate telemetry (#1136)
The keyword gate (#1126) can never know a repo's domain nouns. This adds
the graph-derived tier the design discussion converged on: symbol names
are split into prose segments at index time (name_segment_vocab, riding
the insertNode write path), and the hook verifies a prompt's plain words
against them — "the state machine des commandes" → OrderStateMachine, in
any language whose technical nouns are Latin script.

Confidence now decides HOW MUCH to inject, not just whether:
- HIGH (keyword, or index-verified code token): full explore injection,
  unchanged — the validated adoption lever.
- MEDIUM (segment matches only): a ~500-byte pointer naming the matching
  symbols; the AGENT writes the explore query. Never runs explore, so a
  fuzzy match can't inject 16KB of wrong-feature context.
- Silent otherwise, as before.

Precision is derived from the repo's own naming statistics plus measured
FP fixes: co-occurrence (≥2 words on one name) always qualifies; a single
word must be ≥5 chars, cluster across 2–25 names (singletons are prose
coincidence: "deploy to production" → matchesNonProductionDir), match a
multi-segment name, and not be an English function/filler word (the one
place a word list is honest: identifiers are English, so only English
prose collides). Every candidate is re-verified against nodes before
being surfaced — vocab rows are proposals, deletions leave orphans by
design, a full index rebuilds from scratch, and sync heals pre-upgrade
databases (batched + yielding; emptiness captured at sync ENTRY so the
sync's own writes can't mask the backfill).

Schema v7 migration is DDL-only (instant; none of the #1067 row-churn
hazards). Gate outcomes roll up as anonymous usage counters
(prompt-hook-gate-<outcome>, names only, never content) through the
existing telemetry pipeline — recall becomes measurable, and the counters
are the agreed kill-criterion data for ever revisiting a local classifier.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:35:38 -05:00
317e7f4d3d fix(prompt-hook): make the structural-question gate multilingual (#1126) (#1134)
* fix(prompt-hook): fire the structural gate for Latin-script, Cyrillic, and JA/KO prompts (#1126)

The prompt-hook's keyword gate only knew English and simplified-Chinese
keywords, so a structural question in French (or Spanish, German, Italian,
Portuguese, Russian, Japanese, Korean, traditional Chinese) silently
no-op'd unless it happened to contain an identifier-shaped code token —
the #994 symptom, resurfaced for every other language.

Root causes fixed:
- JS \b is ASCII-only: a keyword whose first/last char is accented or
  non-Latin (où, qué, Cyrillic, kana) can never match \bkeyword\b —
  the same mechanism behind #994. Keyword matching now uses Unicode
  lookaround boundaries ((?<![\p{L}\p{N}_]) … (?![\p{L}\p{N}_])).
- Bare-stem English entries never matched their own derived forms
  (\barchitect\b can't match "architecture", \bdepend\b can't match
  "dependencies"). Stems are now matched as word prefixes (leading
  boundary only), which also lets one shared stem cover the Romance/
  Germanic spellings that coincide.
- The "CJK" set was simplified-Chinese-only: Japanese (呼び出し, 仕組み,
  実装 — and 追跡 ≠ 追踪), Korean, and traditional-Chinese terms are now
  in the unsegmented substring set.

Code-token extraction and the graph-verification path are unchanged;
non-structural prose stays a zero-cost no-op in every language.

Fixes #1126

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(prompt-hook): extend the gate to tier-2 languages (VI/TR/ID/PL/UA/NL/CS/RO/HU/EL/Nordics/FI/HI/AR/FA/HE/TH)

The first pass covered the 10 largest languages; this closes the rest of
the major-developer-population set (~29 total). Notable per-language
mechanics the curation had to respect:

- Agglutinative languages (Turkish, Finnish, Hungarian) need stems, not
  exact words — suffixes attach to everything (akışı, riippuu, működik).
- Indonesian me-/di-/ber- prefixes block leading-boundary stems, so
  affixed forms are listed explicitly (memanggil, dipanggil, berfungsi).
- Arabic/Farsi/Hebrew are spaced but proclitics attach to the word
  (وكيف = and-how), so they join the substring class with Thai.
- Ukrainian і/и spellings diverge from Russian (архітектур ≠ архитектур).
- Excluded terms that collide with English or code words: NL "pad",
  SV "var", CS "tok", Catalan "com" (matches every .com domain) — with
  regression tests pinning the exclusions.

Vietnamese was the sharpest gap: spaced Latin with heavy diacritics —
exactly the ASCII-\b failure class #1126 reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:34:17 -05:00
04e23917d0 chore(security): remove dead reasoning-offload modules flagged in #1114 (#1132)
The managed-reasoning removal (e5897d03) stripped the CLI/MCP wiring but,
despite its stated intent, left the offload modules and their test suite
behind. The dead code still shipped compiled inside the platform bundles,
and its Windows browser-opener was flagged by a security report (#1114)
for routing the login URL through `cmd /c start`, where cmd re-parses
shell metacharacters. Unreachable since 2026-06-20 and never wired in any
tagged release — but delete it for real: src/reasoning/ (config,
credentials, login, reasoner), __tests__/offload.test.ts, the now-inert
CODEGRAPH_OFFLOAD_DISABLE guard in dynamic-boundaries.test.ts, and the
stale reasoner reference in the FILE_SECTION_PREFIX comment.

Closes #1114

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:56:02 -05:00
e53968cae8 fix(resolution): gate the Lua/Luau annotation pattern against method-call self-match (#1124) (#1131)
Lua method-call syntax (lg:Log()) is byte-identical to the Luau type-annotation
shape (lg: Logger), and the receiver-type scan starts on the call's own line —
so any PascalCase method call self-matched as "type = Log" before the scan
reached the real declaration, silently dropping the calls edge whenever two or
more classes shared a method name.

The annotation pattern now rejects a capture followed by any of Lua's three
call forms; its leading [\w.] lookahead alternative prevents backtracking from
shrinking the capture to dodge the gate. Gated rather than dropped: the pattern
is the only type source for Luau typed params and annotated locals whose
initializer isn't T.new().

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:35:33 -05:00
cf86fe8198 fix(resolution): extend typed-parameter receiver inference to Rust/Go/Dart/PHP (#1125) (#1130)
Completes the #1125 fix. The same typed-parameter gap fixed for TS/JS existed
in every other language whose localReceiverTypePatterns only matched
keyword-anchored locals (let/var/:=/= new) and never the bare parameter form:

- Rust: the `:`-annotation pattern required `let`, so `fn use(lg: &Logger)`
  didn't match. Dropped the `let` anchor (still covers `let lg: Logger`),
  keeping the `&?mut?` handling — now covers params and closures `|lg: T|`.
- Go: only `lg := T{}` / `var lg T` matched; a parameter/method-receiver
  `func use(lg Logger)` / `func (l Logger) M()` (name-before-type, no keyword)
  didn't. Added a PascalCase-guarded `ident Type` pattern — the guard plus the
  existing enclosing-scope bound (excludes package-level struct fields) keep
  the keyword-free shape from matching unrelated pairs.
- Dart: the type-before-name pattern's trailing `[=;]` missed a parameter's
  `)`/`,`. Widened to `[=;,)]`, mirroring Java/C#.
- PHP: only `$lg = new T` matched; a typed param `function use(Logger $lg)`
  (also `?Logger`, `\App\Logger`, `&$lg`, `catch (E $e)`) didn't. Added a
  type-before-$var pattern. Reserved words can't be class names, so the
  looser lowercase-allowing capture yields no wrong edges.

Every pattern still relies on resolveMethodOnType validating the inferred type
actually declares the method (no edge on a mis-inference) — the same safety
net the already-covered languages use. Verified with a deterministic probe:
all four now disambiguate two same-named methods via the typed param (Java +
Kotlin as passing controls), full suite green (1930), no regressions.

Adds a parameterized regression test (Rust/Go/Dart/PHP), associating method to
type by qualifiedName so it holds where the method sits outside the type's
line range (Rust impl, Go decl).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:17:23 -05:00
385001398b fix(resolution): infer typed-parameter receivers in TS/JS (#1125) (#1129)
The local-variable receiver-type inference from #1108/#1110 covered typed
parameters for every language except TypeScript/JavaScript (+ TSX/JSX). The
TS/JS `:`-annotation pattern required a leading `const|let|var`, so it only
matched a local's own annotation (`const lg: Logger`) and never a bare
parameter (`function use(lg: Logger)` / `(lg: Logger) =>`). With a second
class sharing the method name — the case where a same-name fallback can't
paper over it — `lg.log()` resolved to no edge, dropping it from callers and
impact/blast-radius. TS/JS is the most common language pair in the userbase,
so this was a real precision gap.

Replace the keyword-anchored pattern with the keyword-free
`\b${r}\b\s*:\s*([A-Z][\w.$]*)`, mirroring Kotlin/Swift/Scala. It's a strict
superset (still matches `const lg: Logger`) plus the typed-parameter case,
and the capture stops at `<` so a generic-typed param
(`repo: Repository<User>`) still yields `Repository`. resolveMethodOnType
already validates the inferred type declares the method, so the looser match
produces no edge on a mis-inference — the same safety net the other
languages rely on; Swift already ships this identical bare-colon pattern with
the same theoretical ternary/dict-literal exposure.

Adds a regression test using two ambiguous classes + typed params, asserting
each call routes to its OWN class's method (verified to fail without the fix
and pass with it — a single-class version would pass either way via the
same-name fallback, which is why the collision is load-bearing).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:07:25 -05:00
7c7514f43f fix(sync): degrade auto-sync on a persistent non-lock sync failure (#1127) (#1128)
FileWatcher.flush() bounded only two failure modes — lock contention
(backoff + degrade past MAX_LOCK_RETRIES) and watch-resource exhaustion
(degrade at setup). Its generic catch branch — any *other* sync error —
reset the only circuit breaker (lockRetryCount = 0) and fell through to
scheduleSync() at the normal debounce cadence, forever, with no backoff
and no degrade().

The trigger is realistic, not synthetic: CodeGraph.sync() runs the whole
extract -> resolve -> maintenance pipeline inside try/finally(release) with
no catch, so a deterministic failure (a tree-sitter extractor that crashes
on one file, SQLITE_FULL, an OOM in batched resolution) propagates straight
into that unbounded branch — wedging a long-running daemon/MCP session into
~1,800 failing syncs + log lines/hour while the auto-update guarantee is
silently dead.

Mirror the lock circuit breaker for the generic branch: a separate
consecutive-failure counter (syncFailureRetryCount) reset only by a clean
sync, exponential backoff via the shared finally, and degrade() past
MAX_SYNC_FAILURE_RETRIES with an actionable reason naming the underlying
error. degrade() -> onDegraded/isDegraded() is what surfaces the dead
guarantee (the staleness banner already consumes it) — a lighter flat-retry
would keep it hidden, which is the core of the #876/#1127 complaint.
Reset-on-success means a transient hiccup never degrades.

The lock path is behaviorally unchanged: in any pure-lock scenario
syncFailureRetryCount stays 0, so Math.max(lockRetryCount,
syncFailureRetryCount) and the degrade threshold behave exactly as before.
Renamed MAX_LOCK_RETRY_DELAY_MS -> MAX_RETRY_BACKOFF_MS (shared cap).

Adds two regression tests mirroring the lock-contention ones: a persistent
non-lock failure degrades past the budget; a transient one recovers without
degrading.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:59:22 -05:00
github-actions[bot] 3460accda8 docs(changelog): promote [Unreleased] into [1.2.0]
[skip ci] Auto-generated by Release workflow.
2026-07-02 03:16:38 +00:00
github-actions[bot] 325f59ec47 release: sync package-lock.json to 1.2.0
[skip ci] Auto-generated by Release workflow.
2026-07-02 03:16:29 +00:00
Colby McHenryandClaude Opus 4.8 6c50e968dc chore: bump version to 1.2.0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:07:58 -05:00
358f400c40 feat(resolution): local-variable method calls in Lua, Luau, R, Pascal (#1112) (#1113)
Extends the local-variable receiver-type inference (#1108/#1110) to the
remaining supported languages with object-method calls. An empirical
sweep found Objective-C, Svelte, Vue, and Astro already resolved
`localVar.method()` (ObjC via message-send handling; the template langs
ride the TypeScript path), leaving Lua, Luau, R, and Pascal.

Lua/Luau/R were a resolution gap, not extraction: the call ref IS
extracted (`lg:log`, `lg$log`), but (1) the resolver's fast pre-filter
`hasAnyPossibleMatch` only understood `.`/`::` separators, so a `:`/`$`
ref was dropped before any strategy ran, and (2) matchMethodCall only
parsed `.`/`::` receivers with no local-var inference for these langs.
Fixes: pre-filter now checks the member/receiver around `:` and `$`;
matchMethodCall recognizes `lg:log` / `lg$log` and routes them through
the same inference + validated resolveMethodOnType path; and inference
patterns are added for Lua/Luau (`local x = T.new()` / `T()` / `x: T`),
R (`x <- T$new()`), and Pascal (`var x: T` / `x := T.Create`).

Pascal statement-form calls (`obj.Method;`) now resolve via the new
inference pattern. The assignment-RHS parameterless form
(`x := obj.Method`) is deliberately left as a field read by the existing
Pascal extractor — an intentional field-vs-call ambiguity tradeoff — so
it stays out of scope.

Validated with single-file and two-file same-name repros per language
(resolves to the right method; two-file is same-file-correct, #1079).
Adds all four to the local-variable inference test matrix. Full suite
green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:59:06 -05:00
3424ff36c5 fix(extraction/ruby): build receiver.method calls so instance calls resolve (#1110) (#1111)
The Ruby extractor dropped the method name from a `receiver.method` call:
`lg.log()` was recorded as a call to `lg` (the bare receiver), which
matches no symbol, so the reference resolved to nothing and no method
edge was ever produced. A Ruby method invoked through a receiver had no
recorded callers and was invisible to impact/blast-radius and explore
flow traces. This is the Ruby-specific blocker noted in #1108 — that
local-variable type-inference fix couldn't help Ruby because the call
reference itself was missing.

extractCall recognized receiver-bearing calls by the `object`/`name`/
`function` fields other grammars use; tree-sitter-ruby's `call` node uses
`receiver` + `method`, so it fell through to the generic fallback that
takes the first named child (the receiver) as the callee. Handle Ruby
`call`/`method_call` explicitly: build `receiver.method`, keep bare
`foo(...)` as the method name, emit `Foo.new` as an `instantiates` ref,
and give a capitalized (constant) receiver a `references` edge so a class
used only via its class methods still records a dependent.

With this plus #1108, `lg = Logger.new; lg.log` resolves `lg.log` to
`Logger#log`, and the two-file same-name case is same-file-correct
(#1079). Adds Ruby to the local-variable inference test matrix plus a
focused test asserting `Foo.new` stays an instantiation.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:26:49 -05:00
ed64db08b4 feat(resolution): infer local-variable receiver types across languages (#1108) (#1109)
Instance calls through a local variable — `const lg = new Logger();
lg.log();` — only resolved to the method in C++. Every other language
produced no `calls` edge, because the resolver had no way to learn the
receiver variable's type, so such calls were missing from callers,
impact/blast-radius, and explore flow traces.

Local variables aren't indexed as nodes (node-explosion), so — like the
existing C++ inferrer — this reads the enclosing function's source and
matches the receiver's declaration/initializer to recover its type, then
hands it to resolveMethodOnType. That validates the method actually
exists on the inferred type, so a mis-inference yields no edge, which is
what lets the per-language patterns stay simple. The scan is bounded to
the enclosing scope so a same-named variable in another function can't
leak in.

Generalizes the C++-only path in matchMethodCall into a language dispatch:
C++ keeps its dedicated header-aware inferrer; a new shared
inferLocalReceiverType covers TypeScript, JavaScript, Python, Java, C#,
Kotlin, Swift, Go, Rust, Dart, Scala, and PHP, matching each language's
declaration shapes (`= new T`, `= T(...)`, `= T.new`, `let x = T{}`,
`x := T{}`, `T x = ...`, `x: T`, etc.). For Java/Kotlin an import FQN
still pins which same-named class is meant (#314); other languages fall
back to the call-site's own file (#1079).

Ruby is not covered: its extractor emits no `receiver.method()` call
reference in the first place, so there is nothing for resolution to
resolve — a separate extraction-layer gap.

Adds a parameterized end-to-end test covering all twelve languages. Full
suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:12:22 -05:00
63bc0fd037 fix(resolution): resolve same-named methods to the call site's own file (#1079) (#1107)
When two files each declared a same-named class with a same-named method
(e.g. `class Logger { void log(); }`), a call resolved to whichever
definition was indexed first — so a call in `b/svc` wrongly targeted
`a/svc`, mixing up that method's callers and blast radius.

The reported case was C++ instance calls, but the underlying pattern —
"multiple same-named candidates, pick the first-indexed, ignore the call
site's file" — lived in three resolution paths, each firing for a
different call shape and affecting different languages:

  - `obj.log()`     instance        -> resolveMethodOnType (C++)
  - `Logger.log()`  class receiver  -> matchMethodCall Strategy 1/2/3
                                       (Python, TypeScript, Java, C#)
  - `Logger::log()` qualified       -> matchByQualifiedName (C++, Rust)

All five sites now share one helper, `preferCallSiteFile`, that prefers
a candidate declared in the call site's own file when a name is
ambiguous. It runs after the `preferredFqn` block in resolveMethodOnType,
so Java/Kotlin import disambiguation (#314) — whose target is
intentionally in another file — is unaffected. The helper is a no-op
when there are fewer than two candidates or none share the call site's
file, so the common single-definition case is unchanged.

Adds 8 tests under `Same-name method disambiguation (#1079)`: the
`preferCallSiteFile` contract, resolveMethodOnType precedence (including
a guard that an import FQN still beats the same-file preference),
`matchByQualifiedName` disambiguation, and end-to-end index tests for the
C++ instance, TypeScript static, and C++ qualified call shapes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:55:41 -05:00
43a6fa68f6 fix(graph): complete edge sets & correct node limits in traversal (#1086, #1087, #1088, #1089, #1090)
Three root defects in src/graph/traversal.ts (reported by @inth3shadows as #1086–#1090):

- Depth guard returned before visited.add → duplicate callers/callees at maxDepth=1 and getImpact loop disagreement.
- Dedup gate also gated edge collection → traverseBFS dropped a parallel edge; getImpact dropped a direct incoming dependency edge.
- limit checked per-frame not per-add → high-degree node overshot opts.limit in traverseBFS and dfsRecursive.

traverseBFS now collects every distinct edge among kept nodes (deduped on edge identity), enqueues each node once, and caps per-add. getCallers/getCallees/getImpactRecursive mark visited before the depth check; getImpactRecursive records the incoming edge unconditionally and unifies its loops on visited. 7 regression tests in graph.test.ts, each failing on the pre-fix code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:37:20 -05:00
ed39233f1a fix(index): yield during resolution so the liveness watchdog can't kill a valid large index (#1091) (#1105)
The #850 liveness watchdog SIGKILLs a process whose main-thread event loop
stalls past its window (60s default). It was extended to `index`/`init` in
#999, but reference resolution and callback-edge synthesis run synchronously
on that same thread — so on a large repo a legitimate, in-progress index gets
killed, and users had to disable the watchdog entirely (CODEGRAPH_NO_WATCHDOG=1).

Make the long synchronous spans yield cooperatively so the heartbeat keeps
firing during real work, while a genuinely wedged span (which never reaches a
yield) still trips the watchdog:

- synthesizeCallbackEdges yields between its whole-graph passes, and the heavy
  scanners (closure-collection, event-emitter, JSX-child, object-registry,
  field-channel) yield within their loops;
- batched resolution sub-chunks each batch with yields;
- the deferred chained-call and this-member post-passes yield per ref.

Behaviour-preserving — only timing changes; node/edge counts are identical.

Validated end-to-end with the real watchdog armed at the default 60s: the
released build is SIGKILLed partway through indexing the Swift compiler (27k
files, ~1.1M edges) and the TypeScript compiler, while the fixed build indexes
both to completion.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:13:42 -05:00
6dd5512d8d fix(windows): set windowsHide on remaining child spawns to stop console flash (#1092) (#1104)
On Windows, a black console (conhost) window flashed briefly when CodeGraph
ran as a background MCP server. Several child spawns were missing
`windowsHide: true`, so Windows created a visible console for the child:

- scripts/npm-shim.js — launching the bundled runtime (every server start /
  daemon-idle reconnect) and the self-heal `tar` extraction of a missing
  platform bundle.
- src/reasoning/login.ts — the detached `cmd /c start` browser open.
- src/upgrade/index.ts — package-manager spawn (console-attached, so no flash
  in practice, but set for uniformity: every child spawn now hides).

The daemon spawn (#411) and all git execFileSync sites already set it; this
closes the remaining gaps. Adds an all-platforms source guard to
__tests__/npm-shim.test.ts asserting every spawn in the shim sets windowsHide.

Closes #1092

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:55:29 -05:00
00765200d8 fix(extraction): broaden the curated C++ inline-macro library list (#1103)
* fix(extraction): broaden the curated C++ inline-macro library list

Since #1102 the post-parse salvage already recovers the NAME for any macro, so
adding a library now buys full return-type recovery for it. Extend the curated
list across the major C++ ecosystem: Mozilla/SpiderMonkey, Protobuf, {fmt},
Hedley + nlohmann/json, GLM, Bullet (SIMD_FORCE_INLINE), Skia, OpenCV, EASTL,
Cocos2d-x, Chromium/WebKit (NEVER_INLINE), GLib, SQLite, and the unambiguous
Windows calling conventions (WINAPI / APIENTRY / STDMETHODCALLTYPE / WINAPIV —
which sit between the return type and the name, so blanking them recovers the
return type, e.g. `HRESULT WINAPI Foo()` -> Foo : HRESULT).

Every entry is an exact, curated token matched only in specifier position, so a
real all-caps return type is never touched. Anything still missed keeps its name
via the universal salvage. CARLA control unchanged (440->6 mangles, 0
regressions — none of these libs appear there, confirming no collateral). Eleven
representative full-recovery tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note broadened C++ inline-macro library coverage (#1103)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:43:42 -05:00
cb20a3bf7f feat(extraction): universal recovery of macro-mangled C/C++ function names (#1102)
* feat(extraction): universal recovery of macro-mangled C/C++ function names

The curated inline-macro blank list (#1100/#1101) can't enumerate every
library's macro. Add a universal post-parse net so a function is findable by
name regardless of which macro decorates it, plus a batch of common libraries
to the curated list for full name+return-type recovery.

- recoverMangledCppName: after extraction, recover the real identifier from a
  name still mangled by an un-blanked macro (`MACRO Ret name(…)` misparses to
  "Ret name"). It's a new `recoverMangledName` extractor hook wired only onto
  C/C++, applied to every name they produce. Safe by construction: it only
  touches an already-mangled name (an internal space that isn't a legit
  `operator …`/destructor), so a clean name is returned unchanged; guarded
  against the `Ret (name)` parenthesized-name idiom and bare primitives. Scoped
  to C/C++ so Kotlin/Scala backtick identifiers (which legitimately contain
  spaces) are never touched.
- Curated list extended past UE/pugixml/Godot/Boost to Qt (Q_INVOKABLE, …),
  Folly, Abseil, LLVM, V8, Eigen, and rapidjson.

Validated on CARLA (large UE project, 1131 C++/h files) vs the pre-fix baseline:
function-name mangles 440 -> 6, 431 fixed, and — critically — 0 regressions
(the salvage also recovers names that the pre-parse's own non-local error-recovery
shifts would otherwise re-mangle, erasing the 7 shifts seen in #1101). The 6
residual are all the moodycamel `Ret (name)` idiom, correctly left alone. On a
made-up macro with no list entry (`WEBKIT_EXPORT WTFString compute()`), the name
`compute` is still recovered. Full suite green; eleven regression/safety tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note universal C++ macro-mangled name recovery (#1102)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:29:20 -05:00
a164ceae8b fix(extraction): recognize common third-party C++ inline macros, not just UE (#1101)
* fix(extraction): recognize common third-party C++ inline macros, not just UE

Extend blankCppInlineMacros beyond Unreal Engine's FORCEINLINE family to the
inline/linkage macros that vendored third-party libraries define and that
mangle function names the same way:

- pugixml: PUGI__FN / PUGI__FN_NO_INLINE (before the return type) and
  PUGIXML_FUNCTION (linkage macro, between return type and name — the blank
  mechanism handles both positions).
- Godot: _FORCE_INLINE_ / _ALWAYS_INLINE_.
- Boost: BOOST_FORCEINLINE / BOOST_NOINLINE.
- Generic cross-ecosystem hints: ALWAYS_INLINE / FORCE_INLINE / NOINLINE.

The list now drives a single generated alternation (longest-token-first), so
adding a codebase's macro is a one-line change. Still curated exact tokens in
specifier position only — a real all-caps return type like `HRESULT DoIt()` is
never touched (verified by controls).

Validated on CARLA (large UE project, 1131 C++/h files): function-name mangles
440 -> 16 (428 fixed). The 16 residual and 7 clean->mangled shifts are all in
third-party vendored files — chiefly pugixml.cpp, a 12k-line macro amalgamation
where error recovery is non-local, so blanking one of several *stacked* macros
(PUGI__FN + PUGI__UNSIGNED_OVERFLOW …) shifts an already-imperfect extraction.
Normal C++/UE code (ActionRoguelike, ALS) sees zero regressions — blanking a
macro there only helps. Chasing pugixml's internal attribute macros is left out
of scope. Seven regression tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note third-party C++ inline macro recognition (#1101)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:10:37 -05:00
9b2ce1c8f6 fix(extraction): recover C++ function names prefixed by an inline-specifier macro (#1100)
* fix(extraction): recover C++ function names prefixed by an inline-specifier macro

An unknown inline-specifier macro before a function's return type
(`FORCEINLINE FString GetName(…)`) threw tree-sitter into error recovery: the
macro was read as the return type and — for a non-primitive return — the return
type was glued onto the name, so the function was indexed as
`"FString GetName"` instead of `GetName`, unfindable by name and with no caller
links. This is pervasive in Unreal Engine, where inline helpers are written
`FORCEINLINE <ret> <name>(…)` (e.g. ALS's `FORCEINLINE FString GetEnumerationToString`).

Add `blankCppInlineMacros`, a preParse that blanks the known UE inline macros
(`FORCEINLINE`, `FORCENOINLINE`, `FORCEINLINE_DEBUGGABLE`) with equal-length
spaces so byte offsets stay exact and the declaration parses as an ordinary
function — recovering both the real name AND the return type. This is the same
recover-don't-drop approach as blankCppExportMacros (#946/#1061), and the two
are composed into the cppExtractor preParse.

Matched tightly (exact known tokens, only in specifier position — followed by
the identifier that starts the return type/name), so ordinary identifiers, real
all-caps return types (`HRESULT DoIt()`), string literals, expression uses, and
longer words (`FORCEINLINE_COUNT`) are untouched — verified by controls. C++-only;
Kotlin/Scala re-index byte-for-byte identical. Five regression tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note C++ inline-specifier-macro function name fix (#1100)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 08:42:30 -05:00
712a406726 fix(extraction): correct C++ reference-return and conversion-operator method names (#1096)
* fix(extraction): correct C++ reference-return and conversion-operator method names

Two pre-existing C++ name-extraction bugs surfaced while validating the #1093
forward-declaration fix against real Unreal Engine repos (ActionRoguelike, ALS):

1. Inline methods/functions returning a reference were named after the whole
   declarator. `const int& getRef() const {…}` parses with a reference_declarator
   wrapping the function_declarator; extractName unwrapped pointer_declarator but
   not reference_declarator, so the method was named "& getRef() const" instead
   of "getRef" — polluting search and breaking caller linkage. Ubiquitous in UE
   headers (`const FGameplayTagContainer& GetActiveTags() const`). Now the
   reference wrapper is unwrapped alongside the pointer wrapper.

2. User-defined conversion operators were named with their full declarator —
   `operator EALSMovementState() const` — instead of `operator EALSMovementState`,
   so they didn't match the symbolic-overload style (`operator+`) and carried
   `() const` noise. The operator_cast declarator is now named `operator <type>`.

Both are additive and C++-scoped (reference_declarator / operator_cast are C++
grammar nodes). Pointer, value, and out-of-line reference returns, and symbolic
operator overloads, are unchanged. Six regression tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note C++ reference-return and conversion-operator name fixes (#1096)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 06:49:59 -05:00
f856f7ae49 fix(extraction): skip bodiless C++ forward declarations (#1093) (#1095)
A `class Foo;` forward declaration parses as a bodiless class_specifier.
extractStruct (#831) and extractEnum already skip their bodiless forms,
but extractClass did not — so every forward decl across dozens of headers
minted a phantom bodiless `class` node that competed with, and could be
picked as the blast-radius representative over, the single real definition.

Add an opt-in `skipBodilessClass` extractor flag (set only on cppExtractor)
and skip a bodiless class node when it's set, mirroring the struct/enum
skip. The flag keeps this C/C++-scoped: languages where a bodiless class is
a complete definition (Kotlin `class Empty`, Scala `case object`/`trait`)
leave it unset and are unaffected. The body is now resolved once at the top
of extractClass and reused for the member walk.

Regression tests cover the collapse to a single definition, elaborated-type
references creating no phantom, and Kotlin/Scala staying indexed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 06:23:50 -05:00
Colby MchenryandGitHub ad03d24fb9 Fix formatting in README for upgrade instruction 2026-06-30 14:45:08 -05:00
github-actions[bot] da72946d25 docs(changelog): promote [Unreleased] into [1.1.6]
[skip ci] Auto-generated by Release workflow.
2026-06-30 04:42:36 +00:00
fedb5641b4 chore: bump version to 1.1.6 (#1076)
Patch release: installer prunes old version bundles (#1074) and
`codegraph index` rebuilds an oversized index without wedging (#1067).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 23:41:38 -05:00
31a58070c8 fix(install): prune old version bundles instead of piling them up (#1074) (#1075)
install.sh kept each release in its own versions/<v> dir (~50 MB with the
vendored Node runtime) and only moved the `current` symlink, so old versions
accumulated forever across upgrades. Keep only the just-installed version and
delete the rest; `codegraph upgrade` re-runs install.sh, so this covers
upgrades too. The npm-shim self-heal cache (~/.codegraph/bundles/) prunes the
same way. Windows installs overwrite a single dir in place and were never
affected.

Validated real-world on macOS, Linux (Docker/dash), and Windows (VM): a
v1.1.2 -> v1.1.4 install leaves only the latest behind.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 23:35:50 -05:00
9684b3b5a5 fix(index): rebuild a poisoned/oversized index by recreating the DB, not row-DELETE (#1067) (#1073)
Follow-up to #1065/#1066. Those stopped a *new* index from scanning an
ignored gitlink corpus, but a project that had already built the multi-GB
graph before upgrading still couldn't recover: `codegraph index` printed
only "Indexing project" and was then SIGKILLed (137) by the #850 watchdog
~60s later, before scanning even started.

Root cause is not the scanner. `index` cleared the old graph with a
synchronous `DELETE FROM nodes/edges/files`. `nodes` carries an FTS5
`AFTER DELETE` trigger, so deleting ~1.6M rows fires ~1.6M FTS
delete-markers — O(rows), and it grows the WAL further before it can
finish. A deterministic probe puts the DELETE-clear at 20.4s on 1.5M
synthetic nodes (WAL 1.16->2.14GB); at the report's denser ~2.6KB/node WAL
that crosses the 60s main-thread watchdog. `open()` was never the wedge.

A full re-index is documented as "same result as a fresh init", so make it
one: discard the database files and re-initialize, instead of opening the
old DB and DELETE-ing every row.

- db: add removeDatabaseFiles(dbPath) — unlinks codegraph.db + its
  -wal/-shm sidecars (O(1) regardless of size; sidecars best-effort).
- index: add CodeGraph.recreate(projectRoot) — discards the files and
  returns a fresh, empty instance. Never opens or migrates the poisoned
  DB. POSIX unlinks an open file fine (a live daemon heals via
  reopenIfReplaced, #925); a Windows file lock becomes an actionable
  "stop the daemon / remove .codegraph" error.
- cli: `codegraph index` now calls recreate() instead of open()+clear();
  both clear() calls dropped. The public clear() API is unchanged.

This also reclaims the disk the bloated db/-wal were holding.

Validated: deterministic probe (DELETE O(rows) vs recreate O(1)); an
end-to-end run through the built binary recovering a real 800K-node /
419MB poisoned DB in 0.3s with no wedge and the correct small graph; new
unit + CLI regression tests; existing #874 index tests still green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 23:15:41 -05:00