* feat(java): resolve SpringContextUtil.getBeans(X.class) dynamic lookups
* fix(ingestion): make Spring dynamic lookups graph-correct
Capture Java and Kotlin lookups from ASTs and resolve them through scoped type bindings and transitive JVM assignability so emitted INJECTS edges are attributable, cache-safe, and production-tested.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(ingestion): keep Spring lookup capture linear
Reuse Java and Kotlin scope-query call nodes instead of rewalking each AST, cache DI subtype closures, and enforce linear scaling with production-path benchmarks in CI.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): make incremental analyze skip the derived layers it can reuse (#3016)
A warm incremental run only ever wrote a handful of files, but it still
paid for the whole graph on the way out: Leiden ran over every node,
flow extraction re-derived every process, and all FTS indexes were
dropped and rebuilt from scratch. On a small edit that tail dominated
the run, which is why "incremental" did not feel incremental.
Reuse what the previous run already derived when the write plan allows
it. The pipeline holds back community detection and flow extraction
whenever the persisted metadata says this run is a candidate for a
surgical write; the DB keeps its Community/Process rows instead of a
wipe-and-rewrite; and the FTS sweep is narrowed to the indexes the run
actually has to touch.
The bet is placed before the pipeline and settled after it. Any plan
that turns out to need a freshly derived layer — full rebuild, escalated
write, or an incremental diff with deleted files — runs the held-back
phases through `runDeferredDerivedPhases`, against the same graph and
phase outputs, so its output is identical to never having skipped them.
Correctness details worth naming, since each one silently loses data if
got wrong:
- The MEMBER_OF / STEP_IN_PROCESS edges of the changed files are snapshotted
before the DETACH DELETE and reattached after the subgraph load. Both
endpoints are matched by explicit label: `labels(n)[0]` over an unlabelled
match returns an empty string on this engine, which produced a snapshot
that restored nothing.
- The FTS narrowing unions three sets — what the writeback deletes (a DB
probe, because a symbol the edit removed is in no fresh graph but is
still a row), what it inserts (the fresh graph), and what is missing
right now (else a prior escalation's dropped indexes would never come
back). An unreadable index catalog withdraws the narrowing entirely.
- Deletions disqualify reuse outright: persisted derived rows can reference
nodes this run removes, and nothing short of re-deriving can tell which.
Covered by the existing incremental suites, including the
incremental-equals-force byte-equivalence test and the #2589
drop-before-delete ordering test, plus unit tests for the new helpers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(analyze): address #3102 review on derived reuse and FTS narrowing
Re-run Leiden/flows unless the file-hash diff is empty, restore ENTRY_POINT_OF
on the preserve path, always drop class_fts before Spring synthetic Class DML,
and reject seeded duplicate phase names. Prettier and exact FTS drop-ordering
assertions unblock CI and pin the #2589/#3016 contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(analyze): reuse FileHashDiff for derived-layer preserve
Drop the count DTO, share phase-name uniqueness, and remove the File
FTS sentinel that Class already makes unreachable.
Refs #3102
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(analyze): prettier-wrap shouldPreservePersistedDerivedGraph
quality / format failed on the Pick<FileHashDiff> signature wrapping.
Refs #3102
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add Spring DI resolver for @Autowired List<T> injection
Addresses all P0/P1 findings from tri-review (#2200):
- P0: Register INJECTS in RelationshipType union (compiles)
- P0: Rewrite execute() to emit consumer→implementation edges from graph data only
- P1: Register in VALID_RELATION_TYPES, single-pass O(N) indexes
- P1: Java-only gate with early exit on non-Java repos
- P1: Update FULL_ORDER golden test
- 8 unit tests covering all edge cases
* test: make VALID_RELATION_TYPES size assertion array-driven (no hardcoded count)
The security test hardcoded toBe(16) for the relation type count, but PR #2200
added INJECTS, bumping it to 17. Replace the magic number with an
EXPECTED_RELATION_TYPES array whose .length drives the size assertion,
so future additions only need to append to the list.
Fixes CI failure on PR #2200.
* fix(ingestion): thread raw generic field types onto Property nodes so Spring DI matching works (review 4616076037 P0)
Production declaredType is generics-stripped by design (extractSimpleTypeName:
List<Shape> -> "List"), so the spring-di phase's anchored regexes could never
match real extraction output — the phase was a silent no-op on every real Java
repository, while its unit tests passed against hand-built node shapes.
Add FieldInfo.rawDeclaredType captured verbatim from the field's type node
(.text, generics and qualifiers preserved — same precedent as the JVM method
extractor), thread it through both parse-worker Property sites, add it to the
shared NodeProperties contract, and match on rawDeclaredType ONLY (no
declaredType fallback: it can never match real data and would mask future
plumbing regressions as quiet no-ops).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ingestion): gate Spring DI on real injection annotations, honest edge reason (review 4616076037 P1)
Extract Java field annotations (shared extractAnnotations helper, moved
verbatim from the method extractor) onto Property nodes and require
@Autowired or @Inject before a collection field becomes an INJECTS
candidate. Previously every edge's reason string fabricated "@Autowired"
without any annotation ever being checked, and any plain collection field
would have fanned out false edges once matching worked.
@Resource is deliberately excluded: JSR-250 resolves by bean name first
(defaulting to the field name), injecting a single named collection bean —
the opposite of the collect-all-implementers fan-out INJECTS models. Pinned
by a test.
An annotated candidate missing rawDeclaredType now logs an isDev warning
(plumbing-contract breach signal) instead of vanishing silently.
SCHEMA_BUMP 9 -> 10: Property nodes gained rawDeclaredType + annotations;
warm parse caches must invalidate or the DI phase silently no-ops on
replayed pre-upgrade nodes (the #2038 trap).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(ingestion): framework-neutral di phase + language-scoped Spring matcher registry (review 4616076037 P1)
spring-di was the only pipeline phase naming a language in shared
core/ingestion code (DoD.md language rule; the maintainer's direction is a
generic DI solution). Split it:
- di-extractors/spring.ts: the Spring matcher (annotation gate, collection
type parse, @Resource exclusion rationale, framework-specific reason
payload) — language-scoped home, mirroring route-extractors/.
- di-extractors/index.ts: DI_MATCHERS, a single-valued
ReadonlyMap<SupportedLanguages, DiFieldMatcher> mirroring the
SCOPE_RESOLVERS registry shape sanctioned by AGENTS.md. Constructor
injection deliberately out of scope; widen to arrays only when a second
same-language framework lands.
- pipeline-phases/di.ts (renamed from spring-di.ts): framework-neutral —
routes Property nodes to registered matchers by node language via a typed
guard, then runs the unchanged reverse-index fan-out. Zero language or
framework names remain (grep-verified).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ingestion): language- and qualified-name-scoped interface resolution for DI fan-out (review 4616076037 P2)
The interface index was built from ALL Interface nodes regardless of
language, keyed by bare simple name with last-writer-wins overwrite —
a polyglot repo with a TS and a Java 'Shape' could fan Java INJECTS edges
into TypeScript classes, and two same-named Java interfaces in different
packages silently collapsed to whichever parsed last (documented GitNexus
bug class: #2054, PR #1956).
Resolution is now per-language with qualifiedName as the primary key
(Interface nodes already carry package-qualified qualifiedName); dotted
element types resolve via qualifiedName, bare names via a per-language
simple-name index that records ambiguity and fails CLOSED. Ambiguity skips
are observable: DIOutput.ambiguousSkipped + an aggregated isDev debug log,
so 'no DI fields' is distinguishable from 'all candidates ambiguous'.
Same-package tiebreaking is a pinned, documented follow-up.
Order-independence pinned by running collision tests in both insertion
orders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ingestion): depth-aware Spring collection-type parser for idiomatic generics (review 4616076037 P3)
The two anchored regexes silently skipped idiomatic Spring shapes:
Map<Pair<A,B>, IFoo> (nested-generic key broke the [^,]+ split),
List<? extends IFoo> / List<? super IFoo> (bounded wildcards),
java.util.List<IFoo> (qualified wrapper), and whitespace/multi-line
declarations.
Replace them with a small scanner: whitespace normalization, wrapper
matched by last dotted segment, depth-aware top-level-comma split, wildcard
bound stripping, and a final plain-dotted-type-name gate so anything else
(nested-generic elements, arrays, unbounded wildcards, embedded comments,
unbalanced brackets) fails closed. Every accept and reject is documented in
the module docstring and pinned by 27 table-driven cases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(integration): prove Spring DI end-to-end through the real pipeline (review 4616076037 P1)
Both no-op incarnations of this feature shipped with a green unit suite
because every test hand-built the exact graph shape the phase expected —
no test ever ran real Java source through the actual extraction pipeline.
Add test/integration/spring-di-pipeline.test.ts: real .java fixtures via
runPipelineFromRepo, pinning (a) the extraction contract on the annotated
field's Property node (declaredType 'List', rawDeclaredType 'List<IFoo>',
annotations ['@Autowired']), (b) set-equality on ALL INJECTS edges
(exactly Consumer->FooA and Consumer->FooB; the non-annotated 'plain'
field of the same type contributes nothing; no self-edges), and (c) a
negative-control fixture with no injection annotations producing zero
INJECTS edges. Either historical regression fails at least one of these.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(incremental): register INJECTS across product surfaces + delete-before-writeback (review 4616076037 P2)
INJECTS was allowlisted in VALID_RELATION_TYPES but invisible or unhandled
everywhere else. Register it deliberately:
- REL_TYPES (gitnexus-shared schema-constants): web-side validRelType()
otherwise silently rejects INJECTS filters (CLI/web single source of truth).
- mcp/tools.ts cypher edge list (agent-facing schema discovery).
- isGraphWideRelType: INJECTS validity is a whole-program property — a
change to a THIRD file (the interface, or a new/removed implementer)
creates/invalidates edges between two untouched files (the TAINT_PATH /
#2084 M4 U6 class), so incremental extraction must always re-include the
full fresh set.
- deleteAllInjects (lbug-adapter): mirrors deleteAllInterprocTaintPaths —
COUNT-then-DELETE under withConnLock, benign missing-table carve-out,
re-throw otherwise (CodeRelation has no PK and there is no read-side
dedup; a fail-soft delete + re-add would silently duplicate rows).
- run-analyze.ts: the delete is UNCONDITIONAL, next to the Communities
delete — deliberately NOT inside the options.pdg block: the di phase runs
on every persisting analyze while the graph-wide re-include is
unconditional, so a pdg-gated delete would append without deleting on
every non-pdg incremental run (N runs = N copies).
- local-backend.ts comment: opt-in traversal by design (not in default
impact()/context() lists; no IMPACT_RELATION_CONFIDENCE entry per the
WRAPS/FETCHES precedent — edges carry their own 0.8).
- ARCHITECTURE.md: 14 -> 15 phases, DAG diagram, phase table, skip-list.
Note: the tools.ts edge list also predates WRAPS/QUERIES/USES — that drift
is pre-existing and left for a follow-up.
Idempotency pinned end-to-end: two successive incremental runs (real
runFullAnalysis + real LadybugDB, unrelated-file touches) leave the INJECTS
row count stable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: describe INJECTS' actual precondition; drop stale fixed-at-16 comments (review 4616076037 P3)
The shared-schema doc for INJECTS claimed an @Autowired precondition the
code (pre-fix) never checked, and hardwired Spring semantics into what is
now a framework-neutral edge type. Reword: precondition is an injection
annotation recognized by a per-language matcher in di-extractors/;
framework specifics live in the reason payload, not the type contract.
security.test.ts comments still said the allow-list size 'stays fixed at
16' (it is 17 and the assertion derives from EXPECTED_RELATION_TYPES).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: simplify DI surfaces — narrow matcher contract, dedup delete-alls, derive tools edge list
Post-implementation simplification pass (4 review angles):
- DiFieldMatch/CandidateField carried collectionType + matchedAnnotation
that no consumer read (the matcher bakes both into reason) — narrowed to
{elementTypeName, reason}.
- parseElementTypeName had two guard branches fully subsumed by the final
plain-dotted-type-name gate — deleted, rationale folded into the regex
comment.
- The three byte-identical delete-all-by-rel-type functions in lbug-adapter
(TAINT_PATH / CALL_SUMMARY / INJECTS) are now one parameterized helper +
thin wrappers with identical names, signatures, and message text
(character-diff verified) — the missing-table regex and abort policy now
live in exactly one place.
- The cypher tool's hand-maintained edge-type list (already missing
WRAPS/QUERIES/USES) is now derived from the canonical REL_TYPES — the
drift class is gone rather than patched.
- di phase: interface indexes are built only for languages that actually
have candidates; test builder gained a rawDeclaredType opt-out replacing
a hand-rolled node.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: apply Tier-2 review findings — qualified-name fail-closed, honest cypher docs, pinned delete contract, hook isolation
- byQualifiedName was last-writer-wins on duplicate qualified names
(reproduced: order-dependent INJECTS edges with ambiguousSkipped 0 —
same package+interface duplicated across monorepo modules/source roots;
Java qualifiedName has no file-path component). Both indexes now share
the AMBIGUOUS fail-closed sentinel; order-flip test added.
- The REL_TYPES-derived cypher edge list advertised pdg-gated types with
no caveat (LLM queries on them silently return zero rows on default
indexes) — caveat appended, INJECTS example added, impact relationTypes
description now names the DI fan-out opt-in.
- The delete-all re-throw contract (only defense against duplicate
CodeRelation rows) was untested — error classification extracted to a
pure classifyDeleteAllError and pinned exhaustively.
- extractRawType/extractAnnotations hooks lacked the per-hook try/catch
the pipeline applies elsewhere (#2286 pattern): a throwing hook would
silently drop every remaining file in the language group. Hardened,
degradation tested.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(csharp): bind qualified constructor names, capture : base/: this, fix generic strip
Mirrors the Java #1928 parsing-layer fixes for the C# scope-resolution path —
the same three defect classes exist verbatim in C#:
- Qualified / qualified-generic / alias-qualified constructor calls
(`new Ns.Foo()`, `new A.B.Foo()`, `new Ns.Box<int>()`, `new MyAlias::Foo()`,
`new global::Foo()`) bound only `@reference.call.constructor.qualified` with no
`@reference.name`, so the central extractor fell back to the whole-expression
anchor and the reference name became the raw `new Ns.Foo()` text (never
resolved). Derive the simple-name tail via the existing `terminalTypeNameNode`
helper (handles qualified_name, generic tail, and alias_qualified_name), and
add a query arm for the top-level `alias_qualified_name` shape that was not
captured at all.
- `: base(...)` / `: this(...)` explicit constructor initializers, modeled by
tree-sitter as `constructor_initializer` and never matched by the scope query,
dropped the chained-constructor CALLS edges. Synthesize them: `this` → enclosing
type name; `base` → the base type's bare name (first base-list entry, which C#
requires to be the base class). Arity attached for overload disambiguation.
- `interpretCsharpTypeBinding`'s qualifier strip used `lastIndexOf('.')` over the
whole string, cutting inside a qualified generic type ARGUMENT
(`Dictionary<string, Ns.User>` → `User>`). Make stripQualifier generic-aware:
reduce only the segment before the first `<`, re-attaching the generic suffix —
multi-arg generics stay intact so the `.Values`/`.Keys` collection-accessor
unwrap keeps working.
Tests: capture-level unit tests for every constructor shape (incl. alias-qualified,
double-match guard) and `: base`/`: this` (incl. struct/record/mixed-base);
interpretCsharpTypeBinding unit tests (the corruption case + nullable/nested/
unknown-generic edges); end-to-end resolver tests with new fixtures. The
csharp-captures golden was regenerated — drift is purely additive (only the new
fixtures; zero existing-fixture digests changed).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(csharp): enhance constructor resolution and namespace qualification
- Implemented qualified constructor name binding to resolve collisions between types in different namespaces.
- Added support for `: base(...)` and `: this(...)` constructor initializers to ensure correct edge emission in the scope resolution.
- Improved generic argument stripping to prevent incorrect parsing of qualified types.
- Introduced tests for new features, including handling of interface-only base classes and qualified constructor calls.
This update addresses issues related to constructor resolution and namespace qualification, ensuring accurate type references in C# code. Tests have been added to validate these changes.
* fix(csharp): implement namespace prefix tagging for file-level type definitions
- Updated the C# ingestion process to tag file-level type definitions with their enclosing namespace path using a new `namespacePrefix` field, without altering the `qualifiedName`.
- Enhanced the scope resolver to utilize the `namespacePrefix` for resolving same-tail collisions in constructor calls, improving accuracy in type resolution.
- Added unit tests to validate the new functionality, ensuring that namespace prefixes are correctly applied to both block-scoped and file-scoped types, while leaving namespace-free types untagged.
This change addresses issues related to namespace qualification and constructor resolution in C# code, facilitating better handling of type references.
* refactor(scope-resolution): share isOverloadableCallable via util
Extract the ctor/function/method overload predicate into
callable-labels.ts so graph-bridge registration and lookup stay aligned
without duplicated private copies in ids.ts and node-lookup.ts.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
* refactor(ingestion): share a codec for __heritage__/__property__ markers (Ruby + Dart) (#1994)
The Ruby and Dart heritage/property pipelines encoded side-effect facts as ':'-delimited synthetic-import marker strings, hand-constructed and hand-parsed at ~8 sites with the field layout kept in agreement only by a comment — the fragility behind the #1981 edge-drop. Route every site through a single shared codec (utils/heritage-marker.ts: encodeMarker / decodeMarker / isHeritageMarker).
encodeMarker throws on a colon-bearing field so the silent-drop class becomes a loud failure; the ':' wire format is preserved byte-for-byte (ruby-captures-golden unchanged). Language-neutral — keyed only on the literal shared prefixes. Dart already single-sources its prefix and is heritage-only, so its import-target guard is left untouched (no invented __property__ path). Pure refactor: no new edges or behavior.
Verified: new codec unit test; ruby resolver + golden 155/155 (zero golden diff) and dart resolver 63/63 on registry-primary, both green on legacy; tsc + prettier clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(dart): single-source DART_HERITAGE_PREFIX from the shared codec (#1994)
Alias DART_HERITAGE_PREFIX to HERITAGE_MARKER_PREFIX (utils/heritage-marker.ts)
instead of re-declaring the '__heritage__:' literal, so the Dart import-target
heritage guard cannot desync from the codec's encode/decode. Value-identical;
gives the codec prefix a direct production consumer. Addresses the tri-review
nit on PR #2007.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>