Skip to content

feat(javascript): migrate JavaScript to scope-based resolution (RFC #909 Ring 3, issue #928)#1640

Merged
magyargergo merged 10 commits into
abhigyanpatwari:mainfrom
ReidenXerx:feat/javascript-scope-resolution-ring3
May 19, 2026
Merged

feat(javascript): migrate JavaScript to scope-based resolution (RFC #909 Ring 3, issue #928)#1640
magyargergo merged 10 commits into
abhigyanpatwari:mainfrom
ReidenXerx:feat/javascript-scope-resolution-ring3

Conversation

@ReidenXerx

@ReidenXerx ReidenXerx commented May 16, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Implements the RFC #909 Ring 3 scope-resolution pipeline for JavaScript, closing issue #928. JavaScript becomes the fourth language to migrate after TypeScript, Python, and C#.

The new pipeline replaces the legacy call-processor path (controlled by REGISTRY_PRIMARY_JAVASCRIPT, defaulting to true) and passes a full parity gate: 48/48 tests in both legacy=0 and registry-primary=1 modes.


New package: gitnexus/src/core/ingestion/languages/javascript/

Nine new files, each with a focused responsibility:

scope-resolver.ts

Registers javascriptScopeResolver in SCOPE_RESOLVERS. The key flags vs TypeScript:

  • fieldFallbackOnMethodLookup: true — JavaScript is dynamically typed, so the field-fallback heuristic is on (TypeScript keeps it off because its type-binding layer is precise).
  • allowGlobalFreeCallFallback: true — CJS require patterns and global helpers benefit from workspace-wide unique-name fallback.
  • propagatesReturnTypesAcrossImports: true + hoistTypeBindingsToModule: true — JSDoc @returns {T} bindings are synthesized on the function scope, hoisted, and propagated to importers so cross-file chains like const u = getUser(); u.save() resolve correctly.

query.ts

Tree-sitter query for JS scope captures compiled against tree-sitter-javascript. It's a purposeful subset of the TypeScript query — all TypeScript-only node types (interface_declaration, type_annotation, public_field_definition, etc.) are dropped because the JS grammar doesn't define them.

Two important differences from what you might expect:

  • Uses (class) @scope.classclass_expression doesn't exist in the JS grammar and would throw a hard TSQueryErrorNodeType at runtime.
  • Adds (field_definition property: …) @declaration.property for modern class fields (name = value syntax).

captures.ts

emitJsScopeCaptures runs the standard tree-sitter query and then applies four JavaScript-specific synthesis passes:

  1. CJS require() decomposition — Walks the AST for require() calls and synthesizes the same @import.kind/name/alias/source markers that the ESM decomposer produces. This means interpretJsImport delegates unchanged to interpretTsImport for all cases (named, namespace, aliased, side-effect).

  2. JSDoc type bindings — Scans each function-like node for a preceding /** */ block comment, then extracts @param {T} name, @returns {T}, and @type {T} tags. One important subtlety: for export function foo() {}, the JSDoc comment is the sibling of the wrapping export_statement, not the inner function_declaration, so we walk up to the parent when needed.

  3. Constructor field bindings — Walks constructor bodies for this.X = new Y() assignments. Emits @type-binding.class-field anchored inside the constructor so tsBindingScopeFor hoists it to the enclosing Class scope. This makes object-destructuring chains like const { address } = user; address.save() resolve end-to-end. JSDoc @type {T} takes priority over new Y() inference when both are present.

  4. Destructuring / for-of / instanceof synthesis — Shared with TS captures (pure AST logic, no grammar-specific code).

interpret.ts, simple-hooks.ts, merge-bindings.ts, arity.ts, import-target.ts

Thin adapters that delegate to their TypeScript counterparts. Because the synthesizer unifies ESM and CJS shapes, no new interpreter branches are needed anywhere.


Shared pipeline changes

typescript/simple-hooks.ts

tsBindingScopeFor now hoists @type-binding.class-field to the Class scope (mirrors the existing @type-binding.parameter-property branch for TypeScript constructor parameter properties). Only fires for JavaScript captures, no TypeScript behaviour changes.

typescript/interpret.ts

Assigns source: 'annotation' to @type-binding.class-field so its binding strength (2) correctly overrides constructor-inferred fallbacks (1).

languages/typescript.ts

Wires emitJsScopeCaptures into javascriptProvider.emitScopeCaptures.

registry-primary-flag.ts

Adds JavaScript to MIGRATED_LANGUAGES behind REGISTRY_PRIMARY_JAVASCRIPT env flag (defaults to true).

scope-resolution/pipeline/registry.ts

Registers javascriptScopeResolver.


Bugs found and fixed during implementation

These were discovered while getting the parity gate to green:

Bug Root cause Fix
Hard query error on every JS file (class_expression) doesn't exist in tree-sitter-javascript; should be (class) Updated query
Write-access ACCESSES edges missing field_definition nodes (modern class fields) weren't captured, so they never entered the semantic model Added field_definition pattern to query
Chained call svc.getUser().save() failing synthesizeJsDocBindings skipped method_definition nodes, so @returns on methods was never synthesized Added method_definition branch
JSDoc on export function not found Comment is sibling of export_statement, not the inner function_declaration Walk up to parent export_statement when looking for the preceding comment
Object destructuring resolution failing No mechanism to put User.address → Address in the class scope synthesizeConstructorFieldBindings synthesizes class-field type bindings from constructor bodies
Cross-file u.save() not resolving getUser() in the fixture had no @returns {User}, so propagateImportedReturnTypes had nothing to mirror Added JSDoc annotation to fixture

Test coverage

48 integration tests in test/integration/resolvers/javascript.test.ts, covering 13 scenarios:

Scenario Tests
this-resolution — this.save() routes to the right class, not a same-named method elsewhere 3
Parent/extends — EXTENDS edges, class hierarchy 4
Nullable receiver — User|null stripped, correct CALLS edge 5
Super resolution — super.save() routes to parent class 4
Chained method call — svc.getUser().save() via JSDoc @returns 4
Field type resolution — Property nodes, HAS_PROPERTY edges, static fields 5
Write-access ACCESSES edges — user.name = "Alice" emits write edge (confidence 1.0) 3
Object destructuring — const { address } = user; address.save() resolves 3
Post-fixpoint for-loop replay 1
Cross-file ESM propagation — const u = getUser(); u.save() across two files 5
Method enrichment — HAS_METHOD, EXTENDS, isStatic, CALLS 7
Inherited method resolution (SM-9) — MRO first-wins walk 3
Pipeline skipGraphPhases option 4

Parity gate: REGISTRY_PRIMARY_JAVASCRIPT=0 and REGISTRY_PRIMARY_JAVASCRIPT=1 both pass all 48 tests.


ECMA-262 conformance posture

Tracked in #1646 — LANG-javascript: Remaining work after PR #1640 scope-resolution parity (same format as @magyargergo's C++ tracking issue #1564).

The resolver is a graph-safe under-approximation of ECMAScript name lookup — suppresses rather than guesses when semantics are ambiguous. The full tables of what's implemented and what's intentionally deferred live in #1646, but the headline summary is:

36 ECMA-262 constructs covered in this PR, including: module/class/function scopes, all var/let/const declaration forms, ESM named/default/namespace/re-export/dynamic imports, CJS require() (named, namespace, side-effect), all call shapes (free, member, constructor, super), member write/read access, instanceof narrowing, JSDoc @param/@returns/@type synthesis, constructor field inference (this.X = new Y()), object/array destructuring, for...of iteration, await unwrapping, class inheritance (MRO), JSX components, HOC-wrapped declarations.

Why the remaining 12 gaps are deferred to #1646

Each deferred item falls into one of three categories:

Statically unresolvable — impossible to analyze without runtime information; suppression is the only correct answer:

  • Computed property access obj[expr] (§13.3.3) — expr is a runtime value
  • Symbol.* well-known symbol keys (§6.1.5.1) — keys are object identity, not strings
  • Object.assign(T.prototype, M) mixin patterns — resolving M requires dataflow analysis
  • with and eval scope injection (§14.11, §19.2.1) — deprecated / runtime-only semantics

Significant standalone scope — each is its own PR-sized chunk of work, the same way C++ ADL, SFINAE, and template partial ordering were separate sub-issues after #1520:

  • ES5 T.prototype.m = fn (§10.1.1) — requires detecting the assignment shape, retroactively binding methods to the owning class, and handling aliased prototype references (const p = T.prototype; p.m = fn)
  • Private class fields #field (§15.7.3.1) — requires adding private_property_identifier throughout the query and interpreter, plus deciding the graph identity for private members
  • Parameter destructuring fn({ x }) (§15.1.3) — requires JSDoc @param name matching against the destructured binding, which needs changes in the parameter interpreter
  • Generator/async-generator return types (§15.5.1, §15.8.7) — Iterator/AsyncIterator wrapping is a cross-cutting concern shared with the TypeScript resolver

Correct but low real-world impact — not worth the complexity cost in this PR:

  • Block scopes for let/const (§9.1) — only affects shadow-variable disambiguation; no edges are suppressed today
  • Class static {} initialization blocks (§15.7.3.3) — ES2022+, rare in practice
  • Tagged template literals (§13.2.8) — tag calls are uncommon in call-graph-relevant code

The same deferral philosophy was used for the C++ migration (#935#1564) and the Python migration. Landing a working, flag-gated, fully-tested baseline and iterating in tracked follow-ups is the established pattern for Ring 3 migrations in this repo.


Risk & rollout

  • Feature-flagged behind REGISTRY_PRIMARY_JAVASCRIPT (env var, default true). Setting it to 0 falls back to the legacy call-processor path with zero code changes.
  • No changes to shared ingestion code paths that other languages use, except tsBindingScopeFor and interpretTsTypeBinding — both changes are additive (new else if branches that only fire for the new @type-binding.class-field tag, which is only emitted by the JavaScript captures).
  • Run npx gitnexus analyze after merge to refresh the index.

Checklist

Made with Cursor

…atwari#909 Ring 3, issue abhigyanpatwari#928)

Implements the scope-resolution pipeline for JavaScript, making it the
fourth language to migrate after TypeScript, Python, and C#.

New package: `languages/javascript/` (9 files)

- `scope-resolver.ts` — Registers `javascriptScopeResolver` in
  `SCOPE_RESOLVERS`. Enables `fieldFallbackOnMethodLookup` (JS is
  dynamically typed), `allowGlobalFreeCallFallback` (CJS patterns),
  `propagatesReturnTypesAcrossImports`, and `hoistTypeBindingsToModule`
  so JSDoc @returns bindings reach cross-file callers.

- `query.ts` — Tree-sitter query for JS scope captures compiled against
  `tree-sitter-javascript`. Subset of the TypeScript query with TS-only
  nodes dropped. Key differences: uses `(class)` not `class_expression`,
  adds `field_definition` for modern class fields.

- `captures.ts` — `emitJsScopeCaptures` with four JS-specific synthesis
  passes on top of the shared tree-sitter query:
    1. CJS `require()` decomposition — turns `const { X } = require('./m')`
       and `const X = require('./m')` into the same `@import.*` markers
       that the ESM decomposer produces, so `interpretJsImport` needs
       no new branches.
    2. JSDoc bindings — scans AST nodes for preceding `/** */` comments,
       extracts `@param {T} name` and `@returns {T}` tags, and emits
       synthetic `@type-binding.parameter` / `.return` captures. Handles
       `export function` wrappers by looking at the parent
       `export_statement`'s preceding sibling, not the inner function's.
    3. Constructor field bindings — walks constructor bodies for
       `this.X = new Y()` assignments and emits `@type-binding.class-field`
       anchored inside the constructor (hoisted to Class scope by
       `tsBindingScopeFor`). JSDoc `@type {T}` takes priority over
       `new Y()` inference when both are present.
    4. Destructuring / for-of / instanceof synthesis (shared with TS).

- `interpret.ts`, `simple-hooks.ts`, `merge-bindings.ts`, `arity.ts`,
  `import-target.ts` — thin adapters delegating to their TypeScript
  counterparts. ESM and CJS shapes are unified by the synthesizer, so
  no new interpreter branches are needed.

Shared pipeline changes

- `typescript/simple-hooks.ts` — `tsBindingScopeFor` now hoists
  `@type-binding.class-field` to the enclosing Class scope (mirrors the
  existing `@type-binding.parameter-property` branch).

- `typescript/interpret.ts` — assigns `source: 'annotation'` to
  `@type-binding.class-field` captures so the binding strength
  correctly overrides constructor-inferred fallbacks.

- `languages/typescript.ts` — wires `emitJsScopeCaptures` into
  `javascriptProvider.emitScopeCaptures`.

- `registry-primary-flag.ts` — adds `JavaScript` to `MIGRATED_LANGUAGES`
  behind `REGISTRY_PRIMARY_JAVASCRIPT` env flag (default `true`).

- `scope-resolution/pipeline/registry.ts` — registers
  `javascriptScopeResolver`.

Test fixture fix

- `test/fixtures/cross-file-binding/js-cross-file/src/models.js` —
  added `/** @returns {User} */` to `getUser()` so the cross-file return
  type propagation pass has a binding to mirror into callers.

Tests (48 total, parity gate passes at both legacy=0 and registry-primary=1)

The integration suite `test/integration/resolvers/javascript.test.ts`
covers 13 describe blocks:

  - Pipeline skipGraphPhases option (4 tests)
  - `this`-resolution: `this.save()` inside a method resolves to the
    correct class, not a same-named method on another class (3 tests)
  - Parent/extends resolution: EXTENDS edges + class hierarchy (4 tests)
  - Nullable receiver: `/** @returns {User|null} */` strips null and
    resolves correctly (5 tests)
  - Super resolution: `super.save()` routes to the parent class (4 tests)
  - Chained method call: `svc.getUser().save()` via JSDoc @returns (4 tests)
  - Field type resolution: Property nodes, HAS_PROPERTY edges, static
    fields, field metadata (5 tests)
  - Write-access ACCESSES edges: `user.name = "Alice"` emits write edge
    with confidence 1.0 (3 tests)
  - Object destructuring: `const { address } = user; address.save()`
    resolves through the class-field binding (3 tests)
  - Post-fixpoint for-loop replay: `for...of` iterable resolved after
    fixpoint (1 test)
  - Cross-file ESM binding propagation: `const u = getUser(); u.save()`
    resolves to `User#save` across two files (5 tests)
  - Method enrichment: HAS_METHOD edges, EXTENDS, isStatic, CALLS (7 tests)
  - Inherited method resolution (SM-9): MRO first-wins walk (3 tests)

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented May 16, 2026

Copy link
Copy Markdown

@ReidenXerx is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
9218 9217 0 1 481s

✅ All 9217 tests passed

1 test(s) skipped — expand for details
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 78.58% 29986/38158 N/A% 🟢 ███████████████░░░░░
Branches 67.11% 19148/28529 N/A% 🟢 █████████████░░░░░░░
Functions 83.62% 3013/3603 N/A% 🟢 ████████████████░░░░
Lines 81.89% 27029/33004 N/A% 🟢 ████████████████░░░░

📋 View full run · Generated by CI

The js-consumer-before-provider fixture was missing a JSDoc @returns
annotation on getUser(), preventing the scope-based resolution pipeline
from propagating the return type and resolving u.save() to User#save.

Mirrors the same pattern already applied to js-cross-file/src/models.js,
and matches how the TypeScript equivalent uses an explicit `: User` return
type annotation.

Fixes the CI test failure introduced when JavaScript was added to
MIGRATED_LANGUAGES in the JS scope-resolution migration (PR abhigyanpatwari#1640).

Co-authored-by: Cursor <cursoragent@cursor.com>
@ReidenXerx

Copy link
Copy Markdown
Contributor Author

Following up on @kratos's request for a solid understanding of ECMAScript semantics before review:

Quick note on the spec link

The link you shared (https://pdfa.org/resource/iso-21757-ecmascript/) points to ISO 21757 — ECMAScript for PDF (JavaScript scripting APIs in PDF 2.0 documents). Our resolver targets ECMA-262 (the main ECMAScript Language Specification). Both share the same language core; the PDF annex adds PDF host objects on top. I've anchored the tracking issue to ECMA-262 clause numbers throughout.

Tracking issue

I've opened #1646 following the same format as @magyargergo's C++ tracking issue (#1564):

LANG-javascript: Remaining work after PR #1640 scope-resolution parity → #1646

It documents:

  • A full table of what ECMA-262 constructs are implemented in this PR (with clause references)
  • A table of intentional divergences with the spec clause and suppression rationale for each
  • Four cross-cutting limitations (JSDoc @typedef, @type on let/const, JSDoc union types, @extends/@implements)

Happy to turn any row in the divergences table into a dedicated sub-issue if that's the preferred workflow.

ReidenXerx and others added 2 commits May 18, 2026 14:40
Adds JSDoc @type {T} support for variable declarations:

  /** @type {User} */
  const u = getUser();

Previously only @param and @returns were extracted from JSDoc. Now
@type on a lexical_declaration / variable_declaration emits a
@type-binding.annotation capture (source = 'annotation', strength 2),
which overrides any weaker constructor-inferred or alias bindings on
the same name.

Closes gap §14.3.1/§14.3.2 from the ECMA-262 conformance audit in abhigyanpatwari#1646.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @magyargergo's task in 6m 21s —— View job


Adversarial Production-Readiness Review — PR #1640

Todo

  • Read repo context (DoD.md, AGENTS.md, GUARDRAILS.md)
  • Inspect all new JS files (captures.ts, query.ts, scope-resolver.ts, interpret.ts, etc.)
  • Inspect shared TypeScript changes (typescript/simple-hooks.ts, typescript/interpret.ts)
  • Compare JS adapters against TS counterparts
  • Read integration test file (javascript.test.ts)
  • Run hygiene checks (Unicode, git diff --check, file types)
  • Verify CI status and flag wiring (registry-primary-flag.ts, registry.ts)
  • Review generated docs churn (AGENTS.md, CLAUDE.md, SKILL.md)
  • Synthesize findings and post full review

Review Bar for This PR

DoD.md §2.2 requires no language-specific logic to leak into shared infrastructure without a documented reason. §2.7 requires tests that cover ambiguous/suppression cases, not just positive resolution. DoD §6 "Not Done" signals include "A language-specific concern leaked into shared ingestion code" and "tests do not actually exercise the changed path." These are the primary bars I'm applying.


Current PR State

  • Head SHA verified locally: c0d885c — confirmed matching the PR's visible head.
  • CI (from bot comment): ✅ 9166/9167 tests pass, 0 failures, 1 pre-existing Ruby skip. scope-parity / Discover migrated languages ran and passed per the CI report. CodeQL, Gitleaks, Dependency Review, typecheck, e2e, OS matrix all passed.
  • git diff --check: Clean — no whitespace violations.
  • Unicode scan: No bidirectional or hidden control characters in the diff.

Branch Hygiene Assessment

Verdict: merge-from-main commits present but harmless and merge-safe.

The two merge commits (23d36c7, c0d885c) pulled in main between the PR's feature commits. The feature-specific commits are:

  1. 5d6c7b4 — main JS migration commit
  2. c959bd0 — add @type {T} JSDoc synthesis
  3. b6260db — add missing @returns to consumer-before-provider fixture

The AGENTS.md churn (−67 net lines) is confirmed to come from main's doc refresh that updated the index stats from 4325 → 26675 symbols and consolidated sections. The CLAUDE.md additions are from main picking up generated doc updates. Neither came from the PR author's feature work. The JS migration diff is fully auditable in the feature commits. No rebase required.


Understanding of the Change

JavaScript becomes the 8th language in MIGRATED_LANGUAGES. The implementation correctly follows the established Ring 3 pattern (TypeScript is the closest analog). Nine new files in languages/javascript/ are almost entirely thin adapters — six of them delegate directly to TS counterparts. The substantive new work is in captures.ts (CJS decomposition + JSDoc synthesis + constructor field bindings) and query.ts (JS grammar query). Two TypeScript shared files get additive branches.


Findings


Finding 1 — Stale Documentation in index.ts

Severity: Minor
File: gitnexus/src/core/ingestion/languages/javascript/index.ts:29-31
Evidence: The module barrel comment says @type {T} on variable declarations and @typedef are not yet synthesized (limitation #1). But captures.ts commit c959bd0 added synthesizeJsDocBindings which emits @type-binding.annotation for @type {T} on let/const/var. The @typedef deferral is still accurate, but @type {T} is implemented.
Risk: Misleads the next contributor into thinking a feature needs to be added when it already exists. Low production impact.
Recommended fix: Update line 30 to say @typedef only.
Blocks merge: No. Fix this →


Finding 2 — synthesizeJsDocBindings handles expression_statement nodes without guarding against parameter-binding contamination

Severity: Low-medium
File: gitnexus/src/core/ingestion/languages/javascript/captures.ts:292-294
Evidence:

const isExprStmt = node.type === 'expression_statement';
if (!isFnDecl && !isMethodDef && !isExprStmt && !isLexDecl) continue;

When isExprStmt is true, fnName = null (no return binding emitted), but @param tags ARE extracted and emitted as @type-binding.parameter anchored on the expression_statement node. Consider:

/** @param {User} user */
users.forEach(user => { user.save(); });

The users.forEach(...) call is an expression_statement. The JSDoc @param {User} user would create a user: User binding in the outer scope (not the arrow function's parameter scope). If code in the outer scope also references user, this binding could cause false-positive CALLS resolution.

The isExprStmt branch appears to be an accidental over-inclusion — it would most naturally be useful for assignment expressions like /** @type {T} */ obj.x = val, but those are not function-like and the branch doesn't help here either. There is no test covering this case.
Risk: Low real-world frequency, but could cause false CALLS edges in code patterns like annotated .forEach / .map callbacks with a preceding JSDoc @param.
Recommended fix: Remove isExprStmt from the dispatch condition, or restrict it to handle only assignment expressions where a type annotation makes sense.
Blocks merge: No, but warrants a test and fix. Fix this →


Finding 3 — JavaScript-specific logic injected into TypeScript shared files

Severity: Architectural (medium)
Files: gitnexus/src/core/ingestion/languages/typescript/simple-hooks.ts:61-63, gitnexus/src/core/ingestion/languages/typescript/interpret.ts:230
Evidence:
tsBindingScopeFor (a TypeScript function) now has a branch that only fires for JavaScript captures:

// JS constructor body field (`this.address = new Address()` / JSDoc @type):
if (decl['@type-binding.class-field'] !== undefined) {
  return walkToScope(innermost, tree, 'Class');
}

@type-binding.class-field is only emitted by synthesizeConstructorFieldBindings in JS captures. TypeScript never emits it. Similarly, interpretTsTypeBinding has a source = 'annotation' branch for @type-binding.class-field that is JS-only.

DoD §2.2 states: "No hidden cross-phase coupling; no leaking of language-specific logic into shared infrastructure without a documented architectural reason." The PR description documents this as intentional ("only fires for the new @type-binding.class-field tag, which is only emitted by the JavaScript captures"), and the JS hooks re-export tsBindingScopeFor as jsBindingScopeFor, so the logic is mechanically reachable. However, a TypeScript maintainer modifying tsBindingScopeFor must now know about JavaScript semantics.

The correct fix is to implement these branches independently in javascript/simple-hooks.ts (real implementation, not re-export) and javascript/interpret.ts, so TS files contain only TS-relevant logic. The PR author's comment says "additive" but additive language-specific branches in TS infrastructure is exactly what the DoD warns against.
Risk: Maintainability regression. Unlikely to cause production graph errors today, but will cause confusion and potential bugs on future TS-only changes that inadvertently affect JS behavior.
Recommended fix: Move the @type-binding.class-field handling out of typescript/simple-hooks.ts and typescript/interpret.ts into proper javascript/simple-hooks.ts and javascript/interpret.ts implementations (drop the re-export pattern for these two and implement them directly).
Blocks merge: No, but this is the highest-quality concern here and should be tracked as a follow-up in #1646. Fix this →


Finding 4 — No negative/suppression tests for allowGlobalFreeCallFallback

Severity: Medium
File: gitnexus/test/integration/resolvers/javascript.test.ts
Evidence: allowGlobalFreeCallFallback: true enables workspace-wide unique-name free-call resolution. The implementation in free-call-fallback.ts calls pickUniqueGlobalCallable, which has an explicit V1 comment: "False cross-package edges are possible when the caller does not import the target package." The function returns a candidate only when there is exactly one callable with that name globally — so the uniqueness check does exist. However, there is no test in javascript.test.ts that verifies suppression behavior when two callables share the same name. DoD §2.7 requires negative/ambiguous cases to be covered.
Risk: In production JS repositories with common function names (e.g. createId, handleError, init), a CALLS edge could be emitted to an unrelated function in a different package if it happens to be globally unique. This would produce silent wrong edges under REGISTRY_PRIMARY_JAVASCRIPT=1.
Recommended fix: Add a fixture with two files defining the same function name (e.g. helpers.js and utils.js both define createUser()). Assert that the caller in a third file that calls createUser() without importing either produces no CALLS edge (suppression), not an arbitrary resolution.
Blocks merge: No (the uniqueness check exists and is pre-tested by existing tests for other migrated languages), but the gap should be tracked.


Finding 5 — @type {T} on multi-variable const a = x, b = y binds type to all declarators

Severity: Low
File: gitnexus/src/core/ingestion/languages/javascript/captures.ts:351-364
Evidence:

if (varType !== null) {
  for (const declarator of node.namedChildren) {
    if (declarator === null || declarator.type !== 'variable_declarator') continue;
    const nameNode = declarator.childForFieldName('name');
    ...
    out.push({ '@type-binding.annotation': ..., '@type-binding.type': ...(varType) });

A JSDoc @type {User} before const a = getUser(), b = getOrder() would emit a: User AND b: User. The @type {T} JSDoc pattern is intended for single-variable declarations, but the code applies it to all declarators in the statement. Multi-declarator const a = x, b = y with a @type is uncommon but valid JS.
Risk: If it occurs (unusual but possible in generated or transpiled code), b gets an incorrect type binding, potentially creating a wrong CALLS edge on b.method().
Recommended fix: Either limit to the first declarator when @type is present (single-declarator semantic), or skip emission when node.namedChildren.filter(...).length > 1.
Blocks merge: No.


Finding 6 — Query file comment lists class_expression but the query correctly uses (class) — misleading inline documentation

Severity: Cosmetic
File: gitnexus/src/core/ingestion/languages/javascript/query.ts:17-19
Evidence: The file header says "What IS shared with the TypeScript query: Scope patterns: program, class_declaration, class_expression..." but the actual query uses (class) @scope.class — the JS grammar node type for class expressions is class, not class_expression. The PR body correctly explains this, but the file comment is misleading for future maintainers.
Blocks merge: No.


PR-Specific Assessment Sections

JavaScript Resolver Registration and Rollout

javascriptScopeResolver is correctly exported from scope-resolver.ts, re-exported from index.ts, imported in scope-resolution/pipeline/registry.ts, and keyed at SupportedLanguages.JavaScript in SCOPE_RESOLVERS. JavaScript is added to MIGRATED_LANGUAGES making isRegistryPrimary(JavaScript) default true. The REGISTRY_PRIMARY_JAVASCRIPT=0 override correctly forces the legacy path (verified by the scope-parity CI workflow that runs both modes explicitly). No other language's flag was modified. No duplicate registrations. Pass.

ECMAScript / Static-Analysis Correctness

The query correctly uses (class) for class expressions (JS grammar), (class_declaration) for declarations, (field_definition) for modern class fields, and (method_definition) for methods. All TS-only node types (interface_declaration, type_annotation, public_field_definition) are absent from the query. CJS require() decomposition correctly handles destructured named, aliased, namespace, and side-effect forms; dynamic require(computedPath) is correctly suppressed (non-literal source → continue). Constructor field inference is correctly scoped to direct children of constructor bodies (nested function this assignments are not captured). The JSDoc scanner correctly handles the export_statement wrapping issue. Pass with the caveats in Findings 2 and 5.

Tree-sitter Query / Capture Synthesis

Query is compiled at first use via new Parser.Query(JS_GRAMMAR, JAVASCRIPT_SCOPE_QUERY). An invalid node type would throw at that point — the query compiles successfully as evidenced by all 48 tests passing. The (class) @scope.class and (class_declaration) @scope.class patterns are distinct node types in tree-sitter-javascript (not aliases), so there's no duplication of Class scopes for declarations. JSX patterns in JSX_QUERY_SUFFIX use jsx_self_closing_element and jsx_opening_element which exist in the JS grammar. Pass.

CJS / ESM Import Behavior

synthesizeCjsImports correctly identifies require() calls (bare identifier with text require, string-literal first argument only), emits @import.kind/name/alias/source markers matching the ESM shape expected by interpretTsImport. Named destructuring, aliased ({ X: Y }), namespace (const X = require(...)), and side-effect (require(...) as expression statement) forms are all handled. Dynamic require(computedPath) correctly skips (first arg not string). Nested destructuring (const { X: { Y } } = require(...)) is documented as a known limitation in index.ts. Pass.

JSDoc / Type-Binding Behavior

parseJsDocParams regex /@param\s+\{([^}]+)\}\s+\[?(\w+)\]?/g handles optional [name] syntax. parseJsDocReturn handles both @returns and @return. The regex [^}]+ for the type field stops at } — no backtracking catastrophe risk on any input. Union types (User | null), generics, and qualified names (models.User) are correctly stripped by interpretTsTypeBinding's existing pipeline. Binding strength: JSDoc @type {T} emits source = 'annotation' (strength 2) which correctly overrides constructor inference (source = 'constructor-inferred', strength 1). The export function JSDoc lookup is correct (walks to export_statement). Pass with Findings 2 and 5 noted.

Receiver / Member / this / super / Inheritance Behavior

this.save() resolution uses the synthesized this binding from synthesizeTsReceiverBinding, which is anchored at the method scope — correctly routes to the enclosing class's method, not a global or sibling. super is detected by isSuperReceiver regex which correctly matches super., super(, super[, and bare super. Constructor field this.address = new Address() creates a class-scoped binding via @type-binding.class-fieldtsBindingScopeFor → Class scope. MRO first-wins walk is inherited from the shared buildMro. Static/instance field metadata is populated by the existing field extractor, not the scope resolver — verified by test assertions on isStatic. Pass.

Shared TypeScript Regression Risk

The two TypeScript changes are the @type-binding.class-field branches in tsBindingScopeFor (L61-63) and interpretTsTypeBinding (L230). Both are guarded by the presence of decl['@type-binding.class-field'] which is only ever emitted by synthesizeConstructorFieldBindings in captures.ts. TypeScript's emitTsScopeCaptures never calls synthesizeConstructorFieldBindings and does not emit this tag. Therefore, TypeScript behavior is unchanged. The CI report confirms all 9166 tests pass, which includes the TypeScript resolver test suite. Pass. Architectural concern in Finding 3 stands but doesn't cause regression.

Tests and CI

48 tests covering 13 scenarios across 11 fixture sets. Tests assert specific graph edges (CALLS, EXTENDS, HAS_PROPERTY, HAS_METHOD, ACCESSES, IMPORTS) and exact confidence (1.0) where relevant. The scope-parity CI workflow (ci-scope-parity.yml) auto-discovers JavaScript from MIGRATED_LANGUAGES and runs the test file twice: once with REGISTRY_PRIMARY_JAVASCRIPT=0 and once with REGISTRY_PRIMARY_JAVASCRIPT=1. Both passed per the CI bot comment. The negative test gap (Finding 4) and isExprStmt gap (Finding 2) exist but are follow-up material, not blockers under the existing test bar for Ring 3 migrations.

Security / Performance / Hygiene

  • No new dependencies introduced.
  • JSDoc regex patterns ([^}]+) are bounded — no catastrophic backtracking.
  • The 6 AST synthesis passes in emitJsScopeCaptures each do a single full-tree DFS walk: O(N) per pass, O(6N) total. This is consistent with the TypeScript captures pattern and acceptable for the file sizes encountered in real repositories.
  • Singleton parser/query pattern (_jsParser, _jsQuery) avoids repeated initialization cost.
  • No secrets, tokens, or machine-specific paths committed.
  • git diff --check is clean.
  • No bidirectional Unicode in the diff.

Generated Docs and Branch Hygiene

AGENTS.md changes are confirmed to originate from the merge-from-main commits (index stats update 4325 → 26675 symbols, section consolidation from main's 1.7.0 doc refresh). CLAUDE.md additions are the project's standard generated context block. The .claude/skills/gitnexus/gitnexus-cli/SKILL.md changes are +5/-5 minor rewords from the same doc refresh. None of these obscure the JS migration diff — all 9 new javascript/ files and 6 modified files are clearly attributable to the feature work. No rebase required.


Missing Information / GitHub Visibility Gaps

  • Full check annotation details (CodeQL findings, if any) were not visible from the PR page per @magyargergo's comment. However, the CI bot comment confirms all checks passed — no unresolved annotations.
  • The evaluate-server CI workflow (ci.yml matrix) configuration was not inspected but is unchanged in this PR.

Final Verdict

production-ready with minor follow-ups

The core JavaScript scope-resolution implementation is correct and follows the Ring 3 migration pattern established by TypeScript, Python, and C#. All 48 integration tests pass in both legacy and registry-primary modes under a properly wired CI parity gate, and the CI confirms 9166/9167 tests passing overall. The flag defaulting to true is consistent with all other migrated languages and backed by real parity coverage. The rollback path (REGISTRY_PRIMARY_JAVASCRIPT=0) is trivially available via env var.

The findings above (stale doc in index.ts, over-broad isExprStmt in JSDoc synthesis, JS-specific logic in TypeScript shared files, missing suppression test for allowGlobalFreeCallFallback, multi-declarator @type edge case) are all follow-up material for #1646 — none introduce graph corruption, crashes, or unverified default-on behavior. The highest-quality concern is Finding 3 (JS logic in TS infrastructure), which is architecturally unclean but functionally safe and documented.

@magyargergo

Copy link
Copy Markdown
Collaborator

@ReidenXerx Can you please look into claude's findings before we merge this into main? 🙏

@ReidenXerx

Copy link
Copy Markdown
Contributor Author

@ReidenXerx Can you please look into claude's findings before we merge this into main? 🙏

Yes will do EOD

magyargergo and others added 3 commits May 18, 2026 18:18
Finding 1 — stale index.ts comment:
  @type {T} on variable declarations is now synthesized; update the
  Known Limitations note to list only @typedef as unimplemented.

Finding 2 — isExprStmt over-broad in synthesizeJsDocBindings:
  Removing `isExprStmt` from the gate condition eliminates the false-
  positive @param binding that would be emitted for JSDoc comments
  preceding call-expression statements (e.g. `/** @param {User} user */
  users.forEach(user => { ... })`). The expression_statement branch
  provided no useful emission anyway (fnName stayed null so @returns
  was never emitted either).

Finding 3 — JS-specific logic leaking into shared TypeScript files:
  - Export walkToScope from typescript/simple-hooks.ts so JS hooks can
    reuse the traversal without duplicating it.
  - Replace the javascript/simple-hooks.ts re-export with a proper
    jsBindingScopeFor wrapper that handles @type-binding.class-field
    (hoist to Class scope) and then delegates to tsBindingScopeFor.
  - Remove the @type-binding.class-field branch from tsBindingScopeFor
    (TypeScript never emits this tag).
  - In interpretJsTypeBinding, remap @type-binding.class-field →
    @type-binding.annotation before delegating to interpretTsTypeBinding,
    so source = 'annotation' is set correctly without a JS-specific
    branch in shared TS code. Remove the branch from interpretTsTypeBinding.

Finding 5 (cosmetic) — query.ts header listed class_expression:
  Correct the "What IS shared" comment to say (class) — the actual JS
  grammar node type used in the query — and explain why class_expression
  does not exist in tree-sitter-javascript.

All 48 JS + 236 TS integration tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ReidenXerx

Copy link
Copy Markdown
Contributor Author

All 4 actionable findings addressed — pushed as a single commit (fix(js): address adversarial review findings (4 of 5)).

Finding What changed
1 — stale index.ts comment Updated Known Limitations #1: @type {T} on variable declarations is now listed as implemented; only @typedef remains.
2 — isExprStmt over-broad Removed expression_statement from the synthesizeJsDocBindings gate. It was emitting spurious @param bindings for JSDoc comments on call-expression statements (e.g. /** @param {User} u */ users.forEach(…)). The branch provided no useful output anyway (no @returns was ever emitted for it since fnName stayed null).
3 — JS logic in shared TS files @type-binding.class-field handling moved out of tsBindingScopeFor and interpretTsTypeBinding entirely. walkToScope is now exported from typescript/simple-hooks.ts. javascript/simple-hooks.ts is a proper wrapper (jsBindingScopeFor) instead of a re-export. interpretJsTypeBinding remaps @type-binding.class-field → @type-binding.annotation before delegating to TS. TypeScript files have zero JS-specific branches.
5 — misleading class_expression in query.ts header Comment updated to (class) with an explicit note that class_expression does not exist in tree-sitter-javascript.

Finding 4 (missing suppression test for allowGlobalFreeCallFallback) left as follow-up in #1646 per the review's own verdict ("follow-up material, not a blocker").

All 48 JS + 236 TS integration tests pass after the changes.

@magyargergo magyargergo merged commit b37974f into abhigyanpatwari:main May 19, 2026
30 of 31 checks passed
@magyargergo magyargergo linked an issue May 25, 2026 that may be closed by this pull request
8 tasks
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.

LANG-javascript: Migrate JavaScript to scope-based resolution

2 participants