Skip to content

feat(semantic-enrichment): real declaration names, per-language calls, structured annotations, notable strings - #258

Merged
jeremiepas merged 5 commits into
mainfrom
feature/extraction-semantic-enrichment
Nov 13, 2026
Merged

jeremiepas merged 5 commits into
mainfrom
feature/extraction-semantic-enrichment

Conversation

@jeremiepas

@jeremiepas jeremiepas commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Qualification on Apache Fineract (Java, ~4k files) showed the tree-sitter path produces a structurally correct but semantically hollow graph. This closes the four diagnosed gaps.

The gaps (reproduced on a 20-line annotated Java fixture before any fix)

  1. Declaration names are wrong. A method returning ClientData was labeled ClientData, not its own name — extractIdentifier picked the first identifier-class child, and the return-type type_identifier precedes the name positionally. Every method sharing a return type collided on one base id, and the contains remap (Map.fromList, last-wins) silently dropped containment edges for all but one.
  2. Zero calls edges outside TS/JS. callTypes only matched call_expression/new_expression. Java/C#/PHP/Python/Ruby call nodes never matched, and the callee heuristic ("first identifier in the subtree") returned the receiver of receiver.method(), not the method.
  3. Annotations unstructured. @Path(...), @GET, @Scheduled(...) survived only as the head of a flattened signature that also swallowed the method body — no queryable route, verb, cron, or config key.
  4. Notable strings invisible. System.getenv("X") never reached the graph since at Function granularity (the default) method bodies are opaque.

A fifth, found during diagnosis: bare type_identifier references leaked as nodes with empty ids.

The fix

  • Field-aware AST: TSNodeInfo now carries each child's tree-sitter grammar field name — the Hackage binding already populates it, Core.hs just discarded it. childByField/childrenByField let every other fix below key off grammar semantics instead of positional guessing.
  • Names: extractIdentifier prefers the grammar's name field, with a C/C++ declarator-field-chain fallback and a type-list fallback that prefers identifier over type_identifier. type_identifier removed from apiSurfaceTypes (AVI-1440: references emit no node).
  • Collision fix: the per-file remap now keys on (base id, line), not base id alone, so overloaded methods each keep their own contains edge.
  • calls edges fire for every supported grammar's own call vocabulary (java, c-sharp, php, python, ruby, go, rust, c/cpp, ts/js). Callee resolution is field-based with a rightmost-name rule, so svc.save() targets save, not svc. Call-target indexing now covers class members, not just top-level definitions.
  • Annotations (Java fields, Python/TS decorators) land in a structured nodeExtra.annotations array, plus a references edge to the resolved import and a distillation table (route, httpVerb, cron, fixedDelay, topics, configKeys). Signatures are now declaration headers, not full text.
  • Notable strings: env-reads, @Value placeholders, and @KafkaListener/@RabbitListener topics resolve to canonical env:/config:/topic: nodes with references edges, working at every granularity (including the opaque-body default).

How it was verified

  • Every grammar claim was checked by extracting the exact pinned tarball (per graphos.cabal) and reading src/node-types.json directly — not recollection. Two corrections this surfaced: Ruby's real invocation node is method_call (not bare call); PHP's object_creation_expression has no fields at all.
  • cabal test targeted runs: 153 examples, 0 failures, 1 pre-existing pending, across every touched describe block. cabal build all is clean across every component.
  • Two real-grammar hspec checks (not hand-built fixtures) run the actual pinned Java and Python parsers end-to-end.
  • Full before/after rerun against apache/fineract itself (7250 .java files, shallow clone) — a main-built binary vs this branch's binary, identical flags, identical checkout. See openspec/changes/extraction-semantic-enrichment/FINERACT_VERIFICATION.md for the complete table. Headline, at real-world scale: calls edges 0 → 20,709; methods mislabeled with a bare return-type name 5,803/40,059 (14.5%) → 0; type_identifier leak nodes 2,824 → 0; 33,920 nodes gained a structured annotations array (829 httpVerb, 782 route, 37 configKeys, 1 topics); 39 canonical Env/Config/Topic nodes appeared where there were none.

Bugs found and fixed along the way (not in the original plan)

  • Java field_declaration.declarator → variable_declarator.name: the field-name fix for methods had the same positional bug for fields (private String tenantHost; labeled String).
  • Ruby's method/singleton_method were missing from functionBoundaryTypes, so every Ruby call's enclosing scope resolved to the module instead of the method.
  • The env-read string literal lives inside the call's arguments-field value (an argument_list node), not as a direct child of the call — the first version silently matched zero env-reads until this was corrected.

Known limitations (see openspec/changes/extraction-semantic-enrichment for full detail)

  • Cross-file resolution for Java/Kotlin/C#/PHP/Perl is same-file only — the import-binding map calls/references share only understands TS/JS/Python's quoted import shape; these five grammars' unquoted form belongs to the separate Imports.hs pipeline, not wired in here.
  • PHP has no annotation support: the pinned grammar has zero attribute-syntax nodes.
  • Rust attribute_item/C# attribute_list are not wired into nodeExtra.annotations this pass.
  • C# field names are unconfirmed (no node-types.json vendored for it) — falls back to positional scan.
  • Kotlin vendoring was investigated and found genuinely blocked, not merely deferred. The only fwcd/tree-sitter-kotlin revision that generates at the required ABI (tag 0.2.11, 2021) has a real parsing bug (return foo(x) never produces a call_expression), unchanged through every release to date; the upstream fix requires ABI 14, which the Haskell tree-sitter package (latest: 0.9.0.3) can't load — the same ceiling already documented for cpp/nix. Closing this needs a cross-cutting ABI upgrade across every vendored grammar, not a Kotlin-only fix. Full investigation in tasks.md task 6.1.
  • Also found while cross-checking the Fineract run (documented, not fixed — pre-existing, out of scope): identifier-less Tier-3 nodes (if_statement etc.) share a bare, kind-agnostic, cross-file-collidable id space; and Fineract's @Scheduled(fixedRate=...)/initialDelay usages aren't covered by the cron/fixedDelay-only distillation.

🤖 Generated with Claude Code

jeremie added 2 commits November 13, 2026 22:07
Its code (Calls relation, mkCallEdge, TS-only callTypes) already shipped to
main, but the change folder was never archived, so the calls-edges
requirement never merged into the canonical extraction spec. Archiving now
unblocks the extraction-semantic-enrichment change, whose calls-edges delta
is written as MODIFIED against this exact requirement.
…, structured annotations, notable strings

Fineract qualification showed the tree-sitter path was structurally correct
but semantically hollow: methods labeled by their return type instead of
their own name, zero calls edges outside TS/JS, annotations flattened into
unreadable signature text, and no env/config/topic visibility.

- Field-aware AST: TSNodeInfo now carries each child's tree-sitter grammar
  field name (childByField/childrenByField), surfacing data the binding
  already provided but Core.hs discarded.
- extractIdentifier prefers the grammar's own `name` field (falling back to
  a C/C++ declarator-field chain, then the old type-list scan with
  `identifier` preferred over `type_identifier`), fixing both method and
  field names being mislabeled by their declared type. Removed
  `type_identifier` from apiSurfaceTypes (AVI-1440: references emit no
  node). Fixed the per-file collision remap to key on (base id, line)
  instead of base id alone, so overloaded siblings each keep their own
  `contains` edge instead of one silently shadowing the other.
- `calls` edges now fire for every supported grammar's own call-node
  vocabulary (java, c-sharp, php, python, ruby, go, rust, c/cpp, ts/js), not
  just TS/JS call_expression/new_expression. Callee resolution is
  field-based with a rightmost-name rule for member/qualified calls,
  resolving to the called method rather than the receiver. Call-target
  indexing now covers definitions nested in classes/namespaces, not only
  top-level ones.
- Annotations (Java fields, Python/TS decorators) are captured as a
  structured nodeExtra.annotations array, with a references edge to the
  resolved imported symbol and a distillation table surfacing
  route/httpVerb/cron/fixedDelay/topics/configKeys directly on the node.
  Signatures are now declaration headers (annotations/modifiers/name/
  params/return type) instead of full text that swallowed the body.
- Notable strings: context-gated env-reads, @value config placeholders, and
  @KafkaListener/@RabbitListener topics resolve to canonical corpus-global
  env:/config:/topic: nodes with references edges, working at every
  granularity including opaque function bodies.

See openspec/changes/extraction-semantic-enrichment for the full design,
verified grammar matrix, and known limitations (PHP/Rust/C# annotations,
cross-file resolution for non-TS-style import syntax, Kotlin grammar
vendoring).
jeremie added 3 commits November 13, 2026 22:45
…ask 5.3)

Shallow-cloned apache/fineract (7250 .java files) and ran a main-built
"before" binary (separate worktree) against this change's "after" binary on
the identical checkout, same flags.

Confirms every diagnosed gap at real-world scale: 0 -> 20,709 calls edges,
5,803/40,059 (14.5%) -> 0 return-type-mislabeled methods, 2,824 -> 0
type_identifier leak nodes, 0 -> 33,920 nodes with structured annotations
(829 httpVerb, 782 route, 37 configKeys, 1 topics), 0 -> 39 canonical
Env/Config/Topic nodes.

Also found and documented (not fixed, pre-existing, out of scope): an
identifier-less Tier-3 node (if_statement etc.) gets baseId = "" with no
per-file prefix, so its id after #L-line suffixing can collide across files
sharing a line number in the corpus-wide merge; and @scheduled(fixedRate=...)
/initialDelay isn't covered by namedArg (only cron/fixedDelay are).
…g) as investigated and blocked

Tried vendoring fwcd/tree-sitter-kotlin at tag 0.2.11, the only revision
whose grammar.js generates at LANGUAGE_VERSION 11 with this repo's CLI
0.16.9 process. It has a real bug, not a cosmetic one: `return foo(x)` never
parses as a call_expression (confirmed via tree-sitter parse: the identical
call as a bare statement or a property RHS parses correctly; wrapped in
`return` it does not), unchanged across every release from 2021 through at
least 2026-04 (grammar.js had zero commits in that whole window).

The upstream HEAD fixes it, but requires ABI 14: patching past the one
Unicode-regex blocker hits a second, deeper wall (an unresolved grammar
conflict the old CLI's resolver can't auto-resolve). The actual root
constraint is the Haskell tree-sitter package itself (0.9.0.3, the latest on
Hackage) being capped at ABI 11 - the same ceiling vendor/README.md already
documents for cpp/nix. Not a Kotlin-specific gap, so not a Kotlin-specific
fix; closing it needs a cross-cutting ABI upgrade across every vendored
grammar, tracked as a separate, future proposal.
Resolves a textual-only conflict in openspec/specs/extraction/spec.md: both
this branch (archiving AVI-1447) and main (merged via #259, archiving
AVI-1765) appended independent, non-overlapping requirements at the same
insertion point. Kept both — 11 requirements total, openspec validate
--all --strict passes for spec/extraction and spec/import-resolution.
@jeremiepas
jeremiepas merged commit fa8bc29 into main Nov 13, 2026
8 of 9 checks passed
@jeremiepas
jeremiepas deleted the feature/extraction-semantic-enrichment branch November 13, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant