From b37974fdac0bbbbcc3f1f9057af8c1fc213d2f37 Mon Sep 17 00:00:00 2001 From: DuduPhudu <34869259+ReidenXerx@users.noreply.github.com> Date: Tue, 19 May 2026 08:23:13 +0300 Subject: [PATCH 1/7] feat(javascript): migrate JavaScript to scope-based resolution (RFC #909 Ring 3, issue #928) (#1640) --- .claude/skills/gitnexus/gitnexus-cli/SKILL.md | 10 +- AGENTS.md | 149 +--- CLAUDE.md | 64 ++ .../ingestion/languages/javascript/arity.ts | 12 + .../languages/javascript/captures.ts | 722 ++++++++++++++++++ .../languages/javascript/import-target.ts | 72 ++ .../ingestion/languages/javascript/index.ts | 49 ++ .../languages/javascript/interpret.ts | 45 ++ .../languages/javascript/merge-bindings.ts | 21 + .../ingestion/languages/javascript/query.ts | 421 ++++++++++ .../languages/javascript/scope-resolver.ts | 89 +++ .../languages/javascript/simple-hooks.ts | 48 ++ .../core/ingestion/languages/typescript.ts | 25 + .../languages/typescript/simple-hooks.ts | 5 +- .../core/ingestion/registry-primary-flag.ts | 1 + .../scope-resolution/pipeline/registry.ts | 2 + .../js-consumer-before-provider/b-provider.js | 1 + .../js-cross-file/src/models.js | 1 + 18 files changed, 1623 insertions(+), 114 deletions(-) create mode 100644 gitnexus/src/core/ingestion/languages/javascript/arity.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/captures.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/import-target.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/index.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/interpret.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/merge-bindings.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/query.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/scope-resolver.ts create mode 100644 gitnexus/src/core/ingestion/languages/javascript/simple-hooks.ts diff --git a/.claude/skills/gitnexus/gitnexus-cli/SKILL.md b/.claude/skills/gitnexus/gitnexus-cli/SKILL.md index 24586205e..cd9a83be0 100644 --- a/.claude/skills/gitnexus/gitnexus-cli/SKILL.md +++ b/.claude/skills/gitnexus/gitnexus-cli/SKILL.md @@ -17,11 +17,11 @@ npx gitnexus analyze Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files. -| Flag | Effect | -| ------------------- | ------------------------------------------------------------------------------------------------------- | -| `--force` | Force full re-index even if up to date | -| `--embeddings` | Enable embedding generation for semantic search (off by default) | -| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. | +| Flag | Effect | +| -------------- | ---------------------------------------------------------------- | +| `--force` | Force full re-index even if up to date | +| `--embeddings` | Enable embedding generation for semantic search (off by default) | +| `--drop-embeddings` | Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` preserves them. | **When to run:** First time in a project, after major code changes, or when `gitnexus://repo/{name}/context` reports the index is stale. In Claude Code, a PostToolUse hook detects staleness after `git commit` and `git merge` and notifies the agent to run `analyze` — the hook does not run analyze itself, to avoid blocking the agent for up to 120s and risking KuzuDB corruption on timeout. diff --git a/AGENTS.md b/AGENTS.md index 99fc68bd0..46960b967 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,131 +62,64 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING. # GitNexus — Code Intelligence -Indexed as **GitNexus** (4325 symbols, 10556 relationships, 300 execution flows). Use MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. -> If any tool warns the index is stale, run `npx gitnexus analyze` first. +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. ## Always Do -- **MUST run impact analysis before editing any symbol.** `gitnexus_impact({target: "symbolName", direction: "upstream"})` — report blast radius to the user. -- **MUST run `gitnexus_detect_changes()` before committing** — verify only expected symbols and flows are affected. -- **MUST warn the user** if impact returns HIGH or CRITICAL risk. -- Explore unfamiliar code with `gitnexus_query({query: "concept"})` (process-grouped, ranked) instead of grepping. -- Full context on a symbol: `gitnexus_context({name: "symbolName"})`. - -## When Debugging - -1. `gitnexus_query({query: ""})` — find related execution flows -2. `gitnexus_context({name: ""})` — callers, callees, process participation -3. `READ gitnexus://repo/GitNexus/process/{processName}` — trace flow step by step -4. Regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` - -## When Refactoring - -- **Rename:** `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Graph edits are safe; text_search edits need manual review. -- **Extract/Split:** `gitnexus_context` (incoming/outgoing refs) then `gitnexus_impact` (upstream callers) before moving code. -- **After any refactor:** `gitnexus_detect_changes({scope: "all"})` to verify scope. +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. ## Never Do -- Edit a symbol without running `gitnexus_impact` first. -- Ignore HIGH/CRITICAL risk warnings. -- Rename with find-and-replace — use `gitnexus_rename`. -- Commit without `gitnexus_detect_changes()`. -- Add language-specific behavior to shared ingestion code (`gitnexus/src/core/ingestion/`) — use a `LanguageProvider` hook. Seeing `provider.mroStrategy === 'xxx'` or an import from `languages/xxx.ts` in shared code means stop and add a hook. - -## Tools Quick Reference - -| Tool | When to use | Example | -|------|-------------|---------| -| `list_repos` | Discover indexed repos | `gitnexus_list_repos({})` | -| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` | -| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` | -| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` | -| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` | -| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` | -| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` | -| `api_impact` | Pre-change API route impact | `gitnexus_api_impact({route: "/api/users", method: "GET"})` | -| `route_map` | Route → handler → consumer map | `gitnexus_route_map({})` | -| `tool_map` | MCP/RPC tool definitions | `gitnexus_tool_map({})` | -| `shape_check` | Response shape vs consumer access | `gitnexus_shape_check({route: "/api/users"})` | -| `group_list` | List repo groups | `gitnexus_group_list({})` | -| `group_sync` | Rebuild group Contract Registry | `gitnexus_group_sync({name: "myGroup"})` | -| `query` (group mode) | Cross-repo search in a group (RRF-merged) | `gitnexus_query({repo: "@myGroup", query: "auth"})` | -| `context` (group mode) | 360° view across all member repos | `gitnexus_context({repo: "@myGroup", name: "validateUser"})` | -| `impact` (group mode) | Cross-repo blast radius via Contract Bridge | `gitnexus_impact({repo: "@myGroup", target: "X", direction: "upstream"})` | - -> Group mode: pass `repo: "@"` to fan out across all member repos, or `repo: "@/"` to target a single member (path keys from `group.yaml`). Optional `service: ""` filters by service root. Group-level state (contracts, staleness) lives in the resources table below — there are **no** `group_query` / `group_context` / `group_impact` / `group_contracts` / `group_status` MCP tools. -> -> For a full walkthrough of setting up a group across multiple repos that communicate over gRPC, see [docs/guides/microservices-grpc.md](docs/guides/microservices-grpc.md). - -## Impact Risk Levels - -| Depth | Meaning | Action | -|-------|---------|--------| -| d=1 | WILL BREAK — direct callers/importers | MUST update | -| d=2 | LIKELY AFFECTED — indirect deps | Should test | -| d=3 | MAY NEED TESTING — transitive | Test if critical path | +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. ## Resources | Resource | Use for | |----------|---------| -| `gitnexus://repo/GitNexus/context` | Codebase overview, index freshness | +| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | | `gitnexus://repo/GitNexus/clusters` | All functional areas | | `gitnexus://repo/GitNexus/processes` | All execution flows | | `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | -| `gitnexus://group/{name}/contracts` | Group Contract Registry (provider/consumer rows + cross-links) | -| `gitnexus://group/{name}/status` | Per-member index + Contract Registry staleness report | -## Self-Check Before Finishing +## CLI -1. `gitnexus_impact` was run for all modified symbols -2. No HIGH/CRITICAL warnings were ignored -3. `gitnexus_detect_changes()` confirms expected scope -4. All d=1 dependents were updated - -## Keeping the Index Fresh - -```bash -npx gitnexus analyze # incremental by default; preserves embeddings -npx gitnexus analyze --force # full rebuild from scratch (opt out of incremental) -npx gitnexus analyze --embeddings # also generate embeddings for new/changed nodes -npx gitnexus analyze --drop-embeddings # explicit opt-in to wipe existing embeddings -``` - -`analyze` runs **incrementally by default**. The pipeline still parses every file every run (cross-file resolution requires it), but tree-sitter parsing is **served from a content-addressed cache** under `.gitnexus/parse-cache/` (per-chunk JSON shards plus `index.json`) for chunks whose file contents haven't changed since the last run. Older installs may still have a legacy single file `.gitnexus/parse-cache.json`, which is read for backward compatibility but no longer written. Only changed-file rows (and their importers) are rewritten in LadybugDB; unchanged-file rows are preserved. Output is byte-equivalent to a full rebuild. Pass `--force` to wipe and re-index from scratch (e.g., to recover from a corrupt index, or after upgrading GitNexus). - -The parse cache key is **content-addressed and version-tagged**: it survives `--force` runs, and is automatically invalidated by a `gitnexus` package upgrade (so a new tree-sitter grammar doesn't silently replay stale parse output). Safe to delete the whole `.gitnexus/parse-cache/` directory (and remove any legacy `.gitnexus/parse-cache.json` if present) at any time — it'll be rebuilt on the next analyze. - -Check `.gitnexus/meta.json` `stats.embeddings` (0 = none). A plain `analyze` no longer drops existing vectors — pass `--drop-embeddings` to wipe. - -> Claude Code: PostToolUse hook detects a stale index after `git commit` and `git merge` and prompts the agent to run `analyze`. The hook does not invoke `analyze` itself. - -## CLI Skills - -| Task | Skill file | -|------|-----------| -| Architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | -| Blast radius / "What breaks?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | -| Debugging / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | -| Refactoring | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | -| Tools/resources/schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | -| CLI commands (index, status, clean, wiki) | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | - -## Hook env knobs - -The Claude Code hook (`gitnexus/hooks/claude/gitnexus-hook.cjs` and the mirrored plugin copy under `gitnexus-claude-plugin/hooks/`) honours these env vars. Defaults work for normal installations; set them only to override resolution. All path overrides ignore values that do not exist on disk and fall through to the standard resolution chain. - -| Env var | Type | Default | Purpose | -|---------|------|---------|---------| -| `GITNEXUS_HOOK_CLI_PATH` | path | resolved via package layout / `require.resolve` | Override path to the `gitnexus` CLI entry the hook spawns for `augment`. | -| `GITNEXUS_HOOK_LSOF_PATH` | path | `lsof` on `PATH` (with `/usr/bin/lsof`, `/usr/sbin/lsof`, `/sbin/lsof` fallbacks) | Override POSIX `lsof` location for the DB-lock probe. | -| `GITNEXUS_HOOK_PS_PATH` | path | `ps` on `PATH` (with `/bin/ps`, `/usr/bin/ps` fallbacks) | Override POSIX `ps` location. | -| `GITNEXUS_HOOK_POWERSHELL_PATH` | path | `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe` (then `SysWOW64`, then `powershell.exe` on `PATH`) | Override Windows PowerShell location used by the Restart-Manager probe. | -| `GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS` | integer ms | `1200` | Max wall-clock for the Linux `/proc` fd scan before bailing out to the `lsof` fallback. | -| `GITNEXUS_HOOK_RM_TARGET` | path | derived | Restart-Manager target file (the LadybugDB path under `.gitnexus/`). Set internally by the hook; rarely overridden manually. | -| `GITNEXUS_DEBUG` | boolean (`1`/`true`) | unset | Verbose stderr from the hook: prints discarded augment-stderr prefixes and one-shot `.ps1` load-failure warnings. | +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | +| Work in the Ingestion area (239 symbols) | `.claude/skills/generated/ingestion/SKILL.md` | +| Work in the Extractors area (135 symbols) | `.claude/skills/generated/extractors/SKILL.md` | +| Work in the Components area (112 symbols) | `.claude/skills/generated/components/SKILL.md` | +| Work in the Lbug area (96 symbols) | `.claude/skills/generated/lbug/SKILL.md` | +| Work in the Group area (94 symbols) | `.claude/skills/generated/group/SKILL.md` | +| Work in the Cli area (92 symbols) | `.claude/skills/generated/cli/SKILL.md` | +| Work in the Configs area (92 symbols) | `.claude/skills/generated/configs/SKILL.md` | +| Work in the Type-extractors area (90 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` | +| Work in the Hooks area (88 symbols) | `.claude/skills/generated/hooks/SKILL.md` | +| Work in the Unit area (80 symbols) | `.claude/skills/generated/unit/SKILL.md` | +| Work in the Cpp area (73 symbols) | `.claude/skills/generated/cpp/SKILL.md` | +| Work in the Scope-resolution area (72 symbols) | `.claude/skills/generated/scope-resolution/SKILL.md` | +| Work in the Server area (66 symbols) | `.claude/skills/generated/server/SKILL.md` | +| Work in the Local area (61 symbols) | `.claude/skills/generated/local/SKILL.md` | +| Work in the Wiki area (60 symbols) | `.claude/skills/generated/wiki/SKILL.md` | +| Work in the Workers area (57 symbols) | `.claude/skills/generated/workers/SKILL.md` | +| Work in the Embeddings area (56 symbols) | `.claude/skills/generated/embeddings/SKILL.md` | +| Work in the Typescript area (53 symbols) | `.claude/skills/generated/typescript/SKILL.md` | +| Work in the Storage area (51 symbols) | `.claude/skills/generated/storage/SKILL.md` | +| Work in the Php area (48 symbols) | `.claude/skills/generated/php/SKILL.md` | diff --git a/CLAUDE.md b/CLAUDE.md index af4069fcd..70b3f1a36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,3 +52,67 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g ## GitNexus rules See the ` … ` block in **[AGENTS.md](AGENTS.md)** for the canonical MCP tools, impact analysis rules, and index instructions. + + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | +| `gitnexus://repo/GitNexus/clusters` | All functional areas | +| `gitnexus://repo/GitNexus/processes` | All execution flows | +| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | +| Work in the Ingestion area (239 symbols) | `.claude/skills/generated/ingestion/SKILL.md` | +| Work in the Extractors area (135 symbols) | `.claude/skills/generated/extractors/SKILL.md` | +| Work in the Components area (112 symbols) | `.claude/skills/generated/components/SKILL.md` | +| Work in the Lbug area (96 symbols) | `.claude/skills/generated/lbug/SKILL.md` | +| Work in the Group area (94 symbols) | `.claude/skills/generated/group/SKILL.md` | +| Work in the Cli area (92 symbols) | `.claude/skills/generated/cli/SKILL.md` | +| Work in the Configs area (92 symbols) | `.claude/skills/generated/configs/SKILL.md` | +| Work in the Type-extractors area (90 symbols) | `.claude/skills/generated/type-extractors/SKILL.md` | +| Work in the Hooks area (88 symbols) | `.claude/skills/generated/hooks/SKILL.md` | +| Work in the Unit area (80 symbols) | `.claude/skills/generated/unit/SKILL.md` | +| Work in the Cpp area (73 symbols) | `.claude/skills/generated/cpp/SKILL.md` | +| Work in the Scope-resolution area (72 symbols) | `.claude/skills/generated/scope-resolution/SKILL.md` | +| Work in the Server area (66 symbols) | `.claude/skills/generated/server/SKILL.md` | +| Work in the Local area (61 symbols) | `.claude/skills/generated/local/SKILL.md` | +| Work in the Wiki area (60 symbols) | `.claude/skills/generated/wiki/SKILL.md` | +| Work in the Workers area (57 symbols) | `.claude/skills/generated/workers/SKILL.md` | +| Work in the Embeddings area (56 symbols) | `.claude/skills/generated/embeddings/SKILL.md` | +| Work in the Typescript area (53 symbols) | `.claude/skills/generated/typescript/SKILL.md` | +| Work in the Storage area (51 symbols) | `.claude/skills/generated/storage/SKILL.md` | +| Work in the Php area (48 symbols) | `.claude/skills/generated/php/SKILL.md` | + + diff --git a/gitnexus/src/core/ingestion/languages/javascript/arity.ts b/gitnexus/src/core/ingestion/languages/javascript/arity.ts new file mode 100644 index 000000000..f3702a4b2 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/arity.ts @@ -0,0 +1,12 @@ +/** + * Arity compatibility for JavaScript. + * + * Delegates to `typescriptArityCompatibility` unchanged — JavaScript + * supports the same arity constructs (rest parameters `...args`, default + * parameters `p = v`) and the metadata shape (`parameterCount`, + * `requiredParameterCount`, `parameterTypes`) is synthesized by the same + * `computeTsArityMetadata` function (which understands both TS and JS + * parameter node types via `extractTsJsParameters`). + */ + +export { typescriptArityCompatibility as jsArityCompatibility } from '../typescript/arity.js'; diff --git a/gitnexus/src/core/ingestion/languages/javascript/captures.ts b/gitnexus/src/core/ingestion/languages/javascript/captures.ts new file mode 100644 index 000000000..a04844059 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/captures.ts @@ -0,0 +1,722 @@ +/** + * `emitScopeCaptures` for JavaScript. + * + * Adapts `emitTsScopeCaptures` for the JavaScript grammar: + * + * 1. **JS grammar** — uses `tree-sitter-javascript` instead of + * `tree-sitter-typescript`. The JS scope query is a subset of the + * TypeScript one (TypeScript-only node types dropped). + * + * 2. **CJS `require()` decomposition** — `const { X } = require('./m')` + * and `const X = require('./m')` are walked in a post-query pass and + * synthesized as `@import.kind/name/alias/source` markers so that + * `interpretJsImport` can recover a `ParsedImport` using the same + * shape as the TypeScript ESM decomposer. + * + * 3. **JSDoc type bindings** — JavaScript has no static type annotations + * so `@type-binding.parameter` / `@type-binding.return` must be + * inferred from leading JSDoc comments. A lightweight regex scanner + * (`parseJsDocParams` / `parseJsDocReturn`) extracts `@param {T} n` + * and `@returns {T}` tags and emits synthetic captures positioned on + * the annotated function node. + * + * 4. **Shared synthesis passes** — destructuring, for-of map-tuple, and + * instanceof narrowing passes are duplicated from `typescript/captures.ts` + * (they are pure AST operations with no grammar-specific logic). + * + * Pure given the input source text. No I/O, no globals consulted. + */ + +import type { Capture, CaptureMatch } from 'gitnexus-shared'; +import { + findNodeAtRange, + nodeToCapture, + syntheticCapture, + type SyntaxNode, +} from '../../utils/ast-helpers.js'; +import { splitImportStatement } from '../typescript/import-decomposer.js'; +import { getJsParser, getJsScopeQuery, jsCachedTreeMatchesGrammar } from './query.js'; +import { computeTsArityMetadata } from '../typescript/arity-metadata.js'; +import { synthesizeTsReceiverBinding } from '../typescript/receiver-binding.js'; +import { getTreeSitterBufferSize } from '../../constants.js'; +import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js'; + +/** JS function-like node types that may carry a synthesized `this` binding. + * Kept in sync with the `@scope.function` patterns in `query.ts`. */ +const FUNCTION_NODE_TYPES = [ + 'method_definition', + 'arrow_function', + 'function_expression', + 'function_declaration', + 'generator_function_declaration', +] as const; + +/** Declaration anchors that carry function-like arity metadata. */ +const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.function'] as const; + +/** Callsite anchors that should carry `@reference.arity` + param types. */ +const CALL_TAGS = [ + '@reference.call.free', + '@reference.call.member', + '@reference.call.constructor', +] as const; + +function pickFirstDefined(grouped: CaptureMatch, tags: readonly string[]): Capture | undefined { + for (const tag of tags) { + const cap = grouped[tag]; + if (cap !== undefined) return cap; + } + return undefined; +} + +/** Filter `@reference.read.member` in non-read contexts (same logic as TS). */ +function shouldEmitReadMember(memberNode: SyntaxNode): boolean { + const parent = memberNode.parent; + if (parent === null) return true; + switch (parent.type) { + case 'call_expression': + return parent.childForFieldName('function')?.id !== memberNode.id; + case 'new_expression': + return parent.childForFieldName('constructor')?.id !== memberNode.id; + case 'assignment_expression': + case 'augmented_assignment_expression': + return parent.childForFieldName('left')?.id !== memberNode.id; + case 'jsx_self_closing_element': + case 'jsx_opening_element': + return parent.childForFieldName('name')?.id !== memberNode.id; + default: + return true; + } +} + +/** Find the first JS function-like node at the given range. */ +function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null { + for (const nodeType of FUNCTION_NODE_TYPES) { + const n = findNodeAtRange(rootNode, range, nodeType); + if (n !== null) return n; + } + return null; +} + +/** Infer a callsite argument's static type from literal shapes. */ +function inferArgType(argNode: SyntaxNode): string { + switch (argNode.type) { + case 'number': + return 'number'; + case 'string': + case 'template_string': + return 'string'; + case 'true': + case 'false': + return 'boolean'; + case 'null': + return 'null'; + case 'undefined': + return 'undefined'; + case 'array': + return 'Array'; + case 'object': + return 'object'; + case 'regex': + return 'RegExp'; + case 'new_expression': { + const ctor = argNode.childForFieldName('constructor'); + return ctor?.text ?? ''; + } + default: + return ''; + } +} + +// ─── CJS require() decomposition ───────────────────────────────────────── + +/** + * Walk the AST and synthesize `@import.*` captures for CJS `require()` calls: + * + * - `const { X, Y } = require('./m')` → one match per destructured name, + * `@import.kind = 'named'`, `@import.name = X / Y`. + * - `const X = require('./m')` → `@import.kind = 'namespace'`, + * `@import.alias = X` (the whole module is bound to X). + * - `require('./m')` as a bare expression-statement → side-effect. + * + * CJS named-alias form (`const { X: alias } = require('./m')`) emits + * `@import.kind = 'named-alias'` with `@import.name = X` and + * `@import.alias = alias`. + * + * The synthesized markers are identical to those produced by + * `splitImportStatement` for ESM, so `interpretJsImport` can delegate + * unchanged to `interpretTsImport` for all cases. + */ +function synthesizeCjsImports(root: SyntaxNode, out: CaptureMatch[]): void { + const stack: SyntaxNode[] = [root]; + for (;;) { + const node = stack.pop(); + if (node === undefined) break; + for (const child of node.namedChildren) { + if (child !== null) stack.push(child); + } + + if (node.type !== 'call_expression') continue; + + // Require call: function must be bare identifier "require". + const fn = node.childForFieldName('function'); + if (fn === null || fn.type !== 'identifier' || fn.text !== 'require') continue; + + const argsNode = node.childForFieldName('arguments'); + if (argsNode === null) continue; + + // Source must be a string literal. + const firstArg = argsNode.namedChild(0); + if (firstArg === null || firstArg.type !== 'string') continue; + const rawSource = firstArg.text; // includes surrounding quotes + const source = firstArg.namedChild(0)?.text ?? rawSource.slice(1, -1); + + const parent = node.parent; + + // Case 1: const { X } = require('./m') OR const X = require('./m') + if (parent?.type === 'variable_declarator') { + const nameNode = parent.childForFieldName('name'); + if (nameNode === null) continue; + + if (nameNode.type === 'object_pattern') { + // Destructured: emit one match per specifier. + for (const field of nameNode.namedChildren) { + if (field === null) continue; + if (field.type === 'shorthand_property_identifier_pattern') { + const name = field.text; + out.push({ + '@import.statement': syntheticCapture('@import.statement', node, rawSource), + '@import.kind': syntheticCapture('@import.kind', node, 'named'), + '@import.name': syntheticCapture('@import.name', field, name), + '@import.source': syntheticCapture('@import.source', firstArg, source), + }); + } else if (field.type === 'pair_pattern') { + const key = field.childForFieldName('key'); + const value = field.childForFieldName('value'); + if (key === null || value === null || value.type !== 'identifier') continue; + out.push({ + '@import.statement': syntheticCapture('@import.statement', node, rawSource), + '@import.kind': syntheticCapture('@import.kind', node, 'named-alias'), + '@import.name': syntheticCapture('@import.name', key, key.text), + '@import.alias': syntheticCapture('@import.alias', value, value.text), + '@import.source': syntheticCapture('@import.source', firstArg, source), + }); + } + } + } else if (nameNode.type === 'identifier') { + // Namespace-style: const X = require('./m') → bind whole module to X. + out.push({ + '@import.statement': syntheticCapture('@import.statement', node, rawSource), + '@import.kind': syntheticCapture('@import.kind', node, 'namespace'), + '@import.alias': syntheticCapture('@import.alias', nameNode, nameNode.text), + '@import.source': syntheticCapture('@import.source', firstArg, source), + }); + } + continue; + } + + // Case 2: bare require('./m') — side-effect import. + if (parent?.type === 'expression_statement') { + out.push({ + '@import.statement': syntheticCapture('@import.statement', node, rawSource), + '@import.kind': syntheticCapture('@import.kind', node, 'side-effect'), + '@import.source': syntheticCapture('@import.source', firstArg, source), + }); + } + } +} + +// ─── JSDoc type binding synthesis ──────────────────────────────────────── + +interface JsDocParam { + readonly name: string; + readonly type: string; +} + +/** Extract `@param {Type} name` entries from a JSDoc comment block. */ +function parseJsDocParams(text: string): readonly JsDocParam[] { + const results: JsDocParam[] = []; + // Match @param {Type} name or @param {Type} [name] (optional) + const re = /@param\s+\{([^}]+)\}\s+\[?(\w+)\]?/g; + let m: RegExpExecArray | null; + while ((m = re.exec(text)) !== null) { + results.push({ type: m[1].trim(), name: m[2].trim() }); + } + return results; +} + +/** Extract `@returns {Type}` or `@return {Type}` from a JSDoc comment. */ +function parseJsDocReturn(text: string): string | null { + const m = /@returns?\s+\{([^}]+)\}/.exec(text); + return m ? m[1].trim() : null; +} + +/** Extract `@type {Type}` from a JSDoc comment (variable-level annotation). */ +function parseJsDocType(text: string): string | null { + const m = /@type\s+\{([^}]+)\}/.exec(text); + return m ? m[1].trim() : null; +} + +/** + * Walk the AST and synthesize `@type-binding.*` captures from JSDoc + * comments immediately preceding function declarations / expressions. + * + * Only `/** … *​/` block comments are scanned. Line comments (`//`) are + * intentionally excluded — JSDoc lives in block comments. + * + * Emits: + * - `@type-binding.parameter` for each `@param {T} n` tag. + * - `@type-binding.return` for `@returns {T}` / `@return {T}`. + * - `@type-binding.annotation` for `@type {T}` on `let`/`const`/`var` + * declarations — covers the common `/** @type {User} *​/ const u = …` + * pattern (ECMA-262 §14.3.1/§14.3.2 variable declarations). + * + * The binding is anchored on the function node so `tsBindingScopeFor` + * can hoist method return-type bindings to Module scope (matching the + * TypeScript path where `hoistTypeBindingsToModule: true`). + */ +function synthesizeJsDocBindings(root: SyntaxNode, out: CaptureMatch[]): void { + const stack: SyntaxNode[] = [root]; + for (;;) { + const node = stack.pop(); + if (node === undefined) break; + for (const child of node.namedChildren) { + if (child !== null) stack.push(child); + } + + const isFnDecl = + node.type === 'function_declaration' || node.type === 'generator_function_declaration'; + const isMethodDef = node.type === 'method_definition'; + // Also check lexical_declaration containing an arrow/fn-expression + const isLexDecl = node.type === 'lexical_declaration' || node.type === 'variable_declaration'; + + if (!isFnDecl && !isMethodDef && !isLexDecl) continue; + + // For `export function foo() { ... }`, the JSDoc comment precedes the + // wrapping export_statement, not the inner function_declaration. + // Walk up to the export_statement so the preceding-sibling search finds it. + const lookupNode = + (isFnDecl || isLexDecl) && node.parent?.type === 'export_statement' ? node.parent : node; + + // Find the preceding sibling comment. + let sibling = lookupNode.previousNamedSibling; + while (sibling !== null && sibling.type === 'comment') { + const text = sibling.text; + if (text.startsWith('/**')) { + // Found a JSDoc block. + const params = parseJsDocParams(text); + const retType = parseJsDocReturn(text); + const varType = isLexDecl ? parseJsDocType(text) : null; + + // Determine the anchor node (the function-like node, for hoisting). + const anchor = node; + + for (const p of params) { + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', anchor, p.name), + '@type-binding.type': syntheticCapture('@type-binding.type', anchor, p.type), + '@type-binding.parameter': syntheticCapture('@type-binding.parameter', anchor, '1'), + }); + } + + if (retType !== null) { + // For named functions, use the function name as the binding name so + // `hoistTypeBindingsToModule` knows which function's return type this is. + let fnName: string | null = null; + if (isFnDecl) { + fnName = node.childForFieldName('name')?.text ?? null; + } else if (isMethodDef) { + // method_definition uses `name:` field for the method name + const nameNode = node.childForFieldName('name'); + if (nameNode?.type === 'property_identifier') fnName = nameNode.text; + } else if (isLexDecl) { + const declarator = node.namedChild(0); + const nameNode = declarator?.childForFieldName('name'); + if (nameNode?.type === 'identifier') fnName = nameNode.text; + } + if (fnName !== null) { + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', anchor, fnName), + '@type-binding.type': syntheticCapture('@type-binding.type', anchor, retType), + '@type-binding.return': syntheticCapture('@type-binding.return', anchor, '1'), + }); + } + } + + // @type {T} on let/const/var: `/** @type {User} */ const u = getUser()`. + // Emits annotation-strength binding (source = 'annotation') so it + // overrides any weaker constructor/alias inference on the same name. + if (varType !== null) { + for (const declarator of node.namedChildren) { + if (declarator === null || declarator.type !== 'variable_declarator') continue; + const nameNode = declarator.childForFieldName('name'); + if (nameNode === null || nameNode.type !== 'identifier') continue; + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', nameNode, nameNode.text), + '@type-binding.type': syntheticCapture('@type-binding.type', nameNode, varType), + '@type-binding.annotation': syntheticCapture( + '@type-binding.annotation', + nameNode, + '1', + ), + }); + } + } + + break; + } + sibling = sibling.previousNamedSibling; + } + } +} + +// ─── Destructuring / for-of / instanceof (shared with TS captures) ─────── + +function synthesizeDestructuringBindings(root: SyntaxNode, out: CaptureMatch[]): void { + const stack: SyntaxNode[] = [root]; + for (;;) { + const node = stack.pop(); + if (node === undefined) break; + for (const child of node.namedChildren) { + if (child !== null) stack.push(child); + } + if (node.type !== 'variable_declarator') continue; + const nameNode = node.childForFieldName('name'); + const valueNode = node.childForFieldName('value'); + if (nameNode === null || valueNode === null) continue; + if (nameNode.type !== 'object_pattern') continue; + if (valueNode.type !== 'identifier') continue; + const rhsName = valueNode.text; + for (const fieldNode of nameNode.namedChildren) { + if (fieldNode === null) continue; + if (fieldNode.type === 'shorthand_property_identifier_pattern') { + const localName = fieldNode.text; + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', fieldNode, localName), + '@type-binding.type': syntheticCapture( + '@type-binding.type', + fieldNode, + `${rhsName}.${localName}`, + ), + '@type-binding.destructured': syntheticCapture( + '@type-binding.destructured', + fieldNode, + fieldNode.text, + ), + }); + } else if (fieldNode.type === 'pair_pattern') { + const key = fieldNode.childForFieldName('key'); + const value = fieldNode.childForFieldName('value'); + if (key === null || value === null || value.type !== 'identifier') continue; + const fieldName = key.text; + const localName = value.text; + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', value, localName), + '@type-binding.type': syntheticCapture( + '@type-binding.type', + fieldNode, + `${rhsName}.${fieldName}`, + ), + '@type-binding.destructured': syntheticCapture( + '@type-binding.destructured', + fieldNode, + fieldNode.text, + ), + }); + } + } + } +} + +function synthesizeForOfMapTupleBindings(root: SyntaxNode, out: CaptureMatch[]): void { + const stack: SyntaxNode[] = [root]; + for (;;) { + const node = stack.pop(); + if (node === undefined) break; + for (const child of node.namedChildren) { + if (child !== null) stack.push(child); + } + if (node.type !== 'for_in_statement') continue; + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (left === null || right === null) continue; + if (left.type !== 'array_pattern' || right.type !== 'identifier') continue; + const rhs = right.text; + let slot = 0; + for (const child of left.namedChildren) { + if (child === null || child.type !== 'identifier') continue; + const localName = child.text; + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', child, localName), + '@type-binding.type': syntheticCapture( + '@type-binding.type', + child, + `__MAP_TUPLE_${slot}__:${rhs}`, + ), + '@type-binding.map-tuple-entry': syntheticCapture( + '@type-binding.map-tuple-entry', + child, + String(slot), + ), + }); + slot++; + } + } +} + +function synthesizeInstanceofNarrowings(root: SyntaxNode, out: CaptureMatch[]): void { + const stack: SyntaxNode[] = [root]; + for (;;) { + const node = stack.pop(); + if (node === undefined) break; + for (const child of node.namedChildren) { + if (child !== null) stack.push(child); + } + if (node.type !== 'if_statement') continue; + const cond = node.childForFieldName('condition'); + if (cond === null) continue; + const inner = cond.type === 'parenthesized_expression' ? cond.namedChildren[0] : cond; + if (inner === null || inner.type !== 'binary_expression') continue; + const op = inner.childForFieldName('operator'); + const left = inner.childForFieldName('left'); + const right = inner.childForFieldName('right'); + if (op === null || left === null || right === null) continue; + if (op.type !== 'instanceof') continue; + if (left.type !== 'identifier') continue; + if (right.type !== 'identifier') continue; + const varName = left.text; + const typeName = right.text; + const cons = node.childForFieldName('consequence'); + if (cons === null) continue; + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', cons, varName), + '@type-binding.type': syntheticCapture('@type-binding.type', right, typeName), + '@type-binding.instanceof-narrow': syntheticCapture( + '@type-binding.instanceof-narrow', + cons, + '1', + ), + }); + } +} + +// ─── Constructor field type bindings ───────────────────────────────────── + +/** + * Synthesize class-scope type bindings from `this.X = new Y()` assignments + * inside constructor method bodies. Covers the traditional ES5+ OOP pattern: + * + * class User { + * constructor() { + * /** @type {Address} *\/ + * this.address = new Address(); + * } + * } + * + * The emitted `@type-binding.class-field` is hoisted to the Class scope by + * `tsBindingScopeFor` so that compound-receiver resolution can look up + * `User.address → Address` when resolving `user.address.save()`. + * + * Type source priority: + * 1. JSDoc `@type {T}` comment immediately preceding the statement + * 2. `new Y()` constructor inference + */ +function synthesizeConstructorFieldBindings(root: SyntaxNode, out: CaptureMatch[]): void { + const stack: SyntaxNode[] = [root]; + for (;;) { + const node = stack.pop(); + if (node === undefined) break; + for (const child of node.namedChildren) { + if (child !== null) stack.push(child); + } + // Only process constructor method definitions + if (node.type !== 'method_definition') continue; + const nameNode = node.childForFieldName('name'); + if (nameNode?.text !== 'constructor') continue; + + const body = node.childForFieldName('body'); + if (body === null) continue; + + for (const stmt of body.namedChildren) { + if (stmt === null || stmt.type !== 'expression_statement') continue; + const expr = stmt.namedChild(0); + if (expr === null || expr.type !== 'assignment_expression') continue; + + const left = expr.childForFieldName('left'); + const right = expr.childForFieldName('right'); + if (left === null || right === null) continue; + if (left.type !== 'member_expression') continue; + + const obj = left.childForFieldName('object'); + const prop = left.childForFieldName('property'); + if (obj === null || prop === null) continue; + if (obj.text !== 'this' || prop.type !== 'property_identifier') continue; + + const fieldName = prop.text; + + // Prefer JSDoc @type annotation on the preceding sibling comment. + let typeName: string | null = null; + const prevSib: SyntaxNode | null = stmt.previousNamedSibling; + if (prevSib !== null && prevSib.type === 'comment') { + const m = /@type\s*\{([^}]+)\}/.exec(prevSib.text); + if (m?.[1]) typeName = m[1].trim(); + } + // Fall back to constructor inference from `new Y()`. + if (typeName === null && right.type === 'new_expression') { + const ctor = right.childForFieldName('constructor'); + if (ctor !== null && ctor.type === 'identifier') typeName = ctor.text; + } + if (typeName === null) continue; + + out.push({ + '@type-binding.name': syntheticCapture('@type-binding.name', prop, fieldName), + '@type-binding.type': syntheticCapture('@type-binding.type', prop, typeName), + // Anchor: positioned inside the constructor body so tsBindingScopeFor + // can walk up from the Function (constructor) scope to the Class scope. + '@type-binding.class-field': syntheticCapture('@type-binding.class-field', stmt, '1'), + }); + } + } +} + +// ─── Main emitter ────────────────────────────────────────────────────────── + +export function emitJsScopeCaptures( + sourceText: string, + filePath: string, + cachedTree?: unknown, +): readonly CaptureMatch[] { + let tree = cachedTree as ReturnType['parse']> | undefined; + if (tree !== undefined && !jsCachedTreeMatchesGrammar(tree)) { + tree = undefined; + } + if (tree === undefined) { + tree = parseSourceSafe(getJsParser(filePath), sourceText, undefined, { + bufferSize: getTreeSitterBufferSize(sourceText), + }); + } + + const rawMatches = getJsScopeQuery(filePath).matches(tree.rootNode); + const out: CaptureMatch[] = []; + + for (const m of rawMatches) { + const grouped: Record = {}; + for (const c of m.captures) { + const tag = '@' + c.name; + grouped[tag] = nodeToCapture(tag, c.node); + } + if (Object.keys(grouped).length === 0) continue; + + // Decompose ESM import_statement / re-export export_statement. + if (grouped['@import.statement'] !== undefined) { + const stmtCapture = grouped['@import.statement']; + const stmtNode = + findNodeAtRange(tree.rootNode, stmtCapture.range, 'import_statement') ?? + findNodeAtRange(tree.rootNode, stmtCapture.range, 'export_statement'); + if (stmtNode !== null) { + const decomposed = splitImportStatement(stmtNode); + for (const d of decomposed) out.push(d); + } + continue; + } + + // Decompose dynamic import() calls. + if (grouped['@import.dynamic'] !== undefined) { + const dynCapture = grouped['@import.dynamic']; + const callNode = findNodeAtRange(tree.rootNode, dynCapture.range, 'call_expression'); + if (callNode !== null) { + const decomposed = splitImportStatement(callNode); + for (const d of decomposed) out.push(d); + } + continue; + } + + // Filter @reference.read.member false-positives. + if (grouped['@reference.read.member'] !== undefined) { + const anchor = grouped['@reference.read.member']; + const memberNode = findNodeAtRange(tree.rootNode, anchor.range, 'member_expression'); + if (memberNode === null || !shouldEmitReadMember(memberNode)) { + continue; + } + } + + // Synthesize arity metadata on function-like declarations. + const declAnchor = pickFirstDefined(grouped, FUNCTION_DECL_TAGS); + if (declAnchor !== undefined) { + const fnNode = findFunctionNode(tree.rootNode, declAnchor.range); + if (fnNode !== null) { + const arity = computeTsArityMetadata(fnNode); + if (arity.parameterCount !== undefined) { + grouped['@declaration.parameter-count'] = syntheticCapture( + '@declaration.parameter-count', + fnNode, + String(arity.parameterCount), + ); + } + if (arity.requiredParameterCount !== undefined) { + grouped['@declaration.required-parameter-count'] = syntheticCapture( + '@declaration.required-parameter-count', + fnNode, + String(arity.requiredParameterCount), + ); + } + if (arity.parameterTypes !== undefined) { + grouped['@declaration.parameter-types'] = syntheticCapture( + '@declaration.parameter-types', + fnNode, + JSON.stringify(arity.parameterTypes), + ); + } + } + } + + // Synthesize @reference.arity on callsites. + const callAnchor = pickFirstDefined(grouped, CALL_TAGS); + if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) { + const callNode = + findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ?? + findNodeAtRange(tree.rootNode, callAnchor.range, 'new_expression'); + if (callNode !== null) { + const argList = callNode.childForFieldName('arguments'); + const args: SyntaxNode[] = + argList === null + ? [] + : argList.namedChildren.filter( + (c): c is SyntaxNode => c !== null && c.type !== 'comment', + ); + grouped['@reference.arity'] = syntheticCapture( + '@reference.arity', + callNode, + String(args.length), + ); + grouped['@reference.parameter-types'] = syntheticCapture( + '@reference.parameter-types', + callNode, + JSON.stringify(args.map(inferArgType)), + ); + } + } + + out.push(grouped); + + // Synthesize `this` receiver type-bindings on class member functions. + const scopeFnAnchor = grouped['@scope.function']; + if (scopeFnAnchor !== undefined) { + const fnNode = findFunctionNode(tree.rootNode, scopeFnAnchor.range); + if (fnNode !== null) { + const synth = synthesizeTsReceiverBinding(fnNode); + if (synth !== null) out.push(synth); + } + } + } + + // Post-query synthesis passes. + synthesizeCjsImports(tree.rootNode, out); + synthesizeJsDocBindings(tree.rootNode, out); + synthesizeConstructorFieldBindings(tree.rootNode, out); + synthesizeDestructuringBindings(tree.rootNode, out); + synthesizeForOfMapTupleBindings(tree.rootNode, out); + synthesizeInstanceofNarrowings(tree.rootNode, out); + + return out; +} diff --git a/gitnexus/src/core/ingestion/languages/javascript/import-target.ts b/gitnexus/src/core/ingestion/languages/javascript/import-target.ts new file mode 100644 index 000000000..bfdfe9951 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/import-target.ts @@ -0,0 +1,72 @@ +/** + * Import-target resolver for JavaScript. + * + * Delegates to the TypeScript `resolveTsTarget` standard-strategy resolver + * with `language: SupportedLanguages.JavaScript` so the resolver tries + * `.js` / `.jsx` extensions in addition to (or instead of) `.ts` / `.tsx`. + * + * The `TsResolveContext.language` flag already exists in `import-target.ts` + * and the resolver (`resolveImportPath`) already branches on it — this + * adapter just wires the right value in. + * + * CJS `require()` calls reference the same module-path strings as ESM + * `import` statements, so the resolver handles them uniformly without any + * CJS-specific logic here. + * + * No `tsconfig.json` path-alias support (JavaScript projects don't use + * `tsconfig.json` compilerOptions.paths in general). Projects that DO use + * tsconfig-based aliases alongside JavaScript can still resolve via the + * standard extension-suffix fallback; the alias branch is a no-op when + * `tsconfigPaths` is null. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import { resolveTsTarget, type TsResolveContext } from '../typescript/import-target.js'; + +export type JsResolveContext = TsResolveContext; + +type PassCache = { + readonly key: ReadonlySet; + readonly allFilePaths: Set; + readonly allFileList: readonly string[]; + readonly normalizedFileList: readonly string[]; + readonly resolveCache: Map; +}; + +/** + * Build a memoized `resolveImportTarget` adapter for JavaScript. + * Caches the derived arrays and per-pass resolve cache across + * `resolveImportTarget` calls within a single workspace pass. + */ +export function makeJsResolveImportTarget(): ( + targetRaw: string, + fromFile: string, + allFilePaths: ReadonlySet, + resolutionConfig?: unknown, +) => string | readonly string[] | null { + let cached: PassCache | null = null; + + return (targetRaw, fromFile, allFilePaths) => { + if (cached === null || cached.key !== allFilePaths) { + const allFileList = Array.from(allFilePaths); + cached = { + key: allFilePaths, + allFilePaths: new Set(allFilePaths), + allFileList, + normalizedFileList: allFileList.map((f) => f.toLowerCase()), + resolveCache: new Map(), + }; + } + + const ws: JsResolveContext = { + fromFile, + language: SupportedLanguages.JavaScript, + allFilePaths: cached.allFilePaths, + allFileList: cached.allFileList, + normalizedFileList: cached.normalizedFileList, + resolveCache: cached.resolveCache, + tsconfigPaths: null, + }; + return resolveTsTarget(targetRaw, ws); + }; +} diff --git a/gitnexus/src/core/ingestion/languages/javascript/index.ts b/gitnexus/src/core/ingestion/languages/javascript/index.ts new file mode 100644 index 000000000..e887bab51 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/index.ts @@ -0,0 +1,49 @@ +/** + * JavaScript scope-resolution hooks (RFC #909 Ring 3, issue #928). + * + * Public API barrel. Consumers should import from this file rather + * than the individual modules. + * + * Module layout (each file is a single concern): + * + * - `query.ts` — JS scope query string + lazy parser/query + * singletons (`getJsParser`, `getJsScopeQuery`) + * - `captures.ts` — `emitJsScopeCaptures` — runs the JS scope query, + * synthesizes CJS require() imports and JSDoc- + * derived type bindings, delegates arity synthesis + * and destructuring/instanceof passes to shared + * or TypeScript utilities + * - `interpret.ts` — `interpretJsImport` / `interpretJsTypeBinding` + * (delegate to TypeScript interpreters — same + * capture-marker vocabulary) + * - `simple-hooks.ts` — `jsBindingScopeFor` (var hoisting), + * `jsImportOwningScope`, `jsReceiverBinding` + * (all delegate to TypeScript counterparts) + * - `merge-bindings.ts` — `jsMergeBindings` (LEGB via typescriptMergeBindings) + * - `arity.ts` — `jsArityCompatibility` (delegates to TS function) + * - `import-target.ts` — `makeJsResolveImportTarget` (memoized adapter) + * - `scope-resolver.ts` — `javascriptScopeResolver` wiring object + * + * ## Known limitations + * + * 1. **JSDoc coverage** — `@param {T} name`, `@returns {T}` / `@return {T}`, + * and `@type {T}` on variable declarations are synthesized. `@typedef` + * is not yet synthesized (tracked in #1646). + * 2. **CJS chained destructuring** — `const { X: { Y } } = require(...)` + * (nested destructuring) emits only the outer `X` binding; `Y` is not + * resolved. + * 3. **Dynamic require** — `require(computedPath)` is skipped (non-literal + * argument — cannot statically resolve the target). + * 4. **`module.exports` / `exports.X`** — CJS export forms are not yet + * modeled as re-exports. The finalize algorithm treats the exporting + * module as a namespace; importers that do `const X = require('./m')` + * bind the module namespace, and member-call resolution walks the + * class graph from there. + */ + +export { emitJsScopeCaptures } from './captures.js'; +export { interpretJsImport, interpretJsTypeBinding } from './interpret.js'; +export { jsMergeBindings } from './merge-bindings.js'; +export { jsArityCompatibility } from './arity.js'; +export { makeJsResolveImportTarget } from './import-target.js'; +export { jsBindingScopeFor, jsImportOwningScope, jsReceiverBinding } from './simple-hooks.js'; diff --git a/gitnexus/src/core/ingestion/languages/javascript/interpret.ts b/gitnexus/src/core/ingestion/languages/javascript/interpret.ts new file mode 100644 index 000000000..7d3688fa9 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/interpret.ts @@ -0,0 +1,45 @@ +/** + * Capture-match → semantic-shape interpreters for JavaScript. + * + * `interpretJsImport` delegates to `interpretTsImport` for all cases + * because `emitJsScopeCaptures` synthesizes the same + * `@import.kind/name/alias/source` markers for both ESM and CJS imports. + * + * The `@import.kind` values emitted for CJS by `captures.ts`: + * + * - `'named'` : `const { X } = require('./m')` → named import + * - `'named-alias'` : `const { X: Y } = require('./m')` → aliased import + * - `'namespace'` : `const X = require('./m')` → namespace import + * - `'side-effect'` : `require('./m')` bare expression → side-effect + * + * These match the kinds `interpretTsImport` already handles for ESM + * (`import { X }`, `import { X as Y }`, `import * as X`, `import './m'`), + * so no new branch is needed here. + * + * `interpretJsTypeBinding` handles the JS-only `@type-binding.class-field` + * tag before delegating to `interpretTsTypeBinding`. The class-field tag + * is emitted by `synthesizeConstructorFieldBindings` and should produce + * `source = 'annotation'` — the same strength as an explicit type + * annotation. Remapping it to `@type-binding.annotation` achieves this + * without adding a JS-specific branch to the shared TS interpreter + * (DoD.md §2.2). + */ + +import type { CaptureMatch, ParsedImport, ParsedTypeBinding } from 'gitnexus-shared'; +import { interpretTsImport, interpretTsTypeBinding } from '../typescript/interpret.js'; + +export function interpretJsImport(captures: CaptureMatch): ParsedImport | null { + return interpretTsImport(captures); +} + +export function interpretJsTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null { + // @type-binding.class-field is a JS-only tag emitted by + // synthesizeConstructorFieldBindings. Remap it to the standard + // @type-binding.annotation tag so interpretTsTypeBinding assigns + // source = 'annotation' without a JS-specific branch in shared code. + if (captures['@type-binding.class-field'] !== undefined) { + const { '@type-binding.class-field': classField, ...rest } = captures; + return interpretTsTypeBinding({ ...rest, '@type-binding.annotation': classField }); + } + return interpretTsTypeBinding(captures); +} diff --git a/gitnexus/src/core/ingestion/languages/javascript/merge-bindings.ts b/gitnexus/src/core/ingestion/languages/javascript/merge-bindings.ts new file mode 100644 index 000000000..271c38684 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/merge-bindings.ts @@ -0,0 +1,21 @@ +/** + * Binding-merge precedence for JavaScript. + * + * JavaScript has no TypeScript declaration-merging (no `interface + class` + * coexisting in the same scope, no `namespace + class` dual-space declarations). + * However, `typescriptMergeBindings` handles these by falling back to + * `['value']` for any `NodeLabel` not explicitly mapped to multiple spaces — + * which is what every JavaScript declaration produces. The result is pure + * LEGB precedence without any cross-space logic, which is exactly what + * JavaScript needs. + * + * Reuse rather than reimplementing to keep the single source of truth for + * the tier (local 0 / import-namespace-reexport 1 / wildcard 2) ordering. + */ + +import type { BindingRef } from 'gitnexus-shared'; +import { typescriptMergeBindings } from '../typescript/merge-bindings.js'; + +export function jsMergeBindings(bindings: readonly BindingRef[]): readonly BindingRef[] { + return typescriptMergeBindings(bindings); +} diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts new file mode 100644 index 000000000..20cfc4f2e --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -0,0 +1,421 @@ +/** + * Tree-sitter query for JavaScript scope captures (RFC §5.1, Ring 3). + * + * Subset of the TypeScript scope query (`languages/typescript/query.ts`) + * compiled against `tree-sitter-javascript`. TypeScript-only node types + * (`interface_declaration`, `type_alias_declaration`, `enum_declaration`, + * `internal_module`, `abstract_class_declaration`, `function_signature`, + * `method_signature`, `abstract_method_signature`, `type_annotation`, + * `public_field_definition`) are dropped because: + * + * 1. The JS grammar doesn't define them — the query compiler would + * throw `InvalidNodeType` if they were included. + * 2. JavaScript has no static type annotations, so the `@type-binding.*` + * patterns derived from TS annotation nodes don't apply. + * + * What IS shared with the TypeScript query: + * + * - Scope patterns: `program`, `class_declaration`, `(class)` (the JS + * grammar node for class expressions — NOT `class_expression`, which + * does not exist in `tree-sitter-javascript`), `function_declaration`, + * `generator_function_declaration`, `function_expression`, + * `arrow_function`, `method_definition`. + * - Declaration patterns for functions, classes, const/let/var, + * object-property arrows (Zustand, TanStack, etc.), and HOC-wrapped + * variable declarations (forwardRef / memo / useCallback / useMemo). + * - Import patterns: `import_statement`, `export_statement` re-exports, + * and dynamic `import()` (represented as `call_expression(import)` in + * both grammars — the `import` leaf node exists in tree-sitter-javascript + * as well as tree-sitter-typescript). + * - Type-binding patterns that work without static annotations: + * constructor inference (`new User()`), call-result alias + * (`const u = getUser()`), member-access alias (`const a = u.addr`), + * identifier alias, assignment rebind, and for-of element bindings. + * JSDoc-derived type bindings (`@param {User} u`, `@returns {User}`) + * are handled separately in `captures.ts` via comment-node scanning. + * - Reference patterns: free calls, member calls, constructor calls, + * write-access, read-access, and dynamic import. + * + * CJS `require()` is NOT captured here; it is handled in `captures.ts` + * by scanning parent context (destructured vs. namespace) of `call_expression` + * nodes whose callee is the identifier `require`. + * + * Grammar version: `tree-sitter-javascript` pinned in gitnexus/package.json. + * + * Exposes lazy `Parser` and `Query` singletons so callers don't pay + * tree-sitter init cost per file. + */ + +import Parser from 'tree-sitter'; +import JS from 'tree-sitter-javascript'; + +const JS_GRAMMAR = JS as Parameters[0]; + +/** True when the file should be parsed with the JSX-extended query. */ +function isJsxFile(filePath: string): boolean { + return filePath.endsWith('.jsx'); +} + +const JAVASCRIPT_SCOPE_QUERY = ` +;; Scopes — module / class-likes / function-likes +(program) @scope.module + +(class_declaration) @scope.class +(class) @scope.class + +(function_declaration) @scope.function +(generator_function_declaration) @scope.function +(function_expression) @scope.function +(arrow_function) @scope.function +(method_definition) @scope.function + +;; Declarations — classes +(class_declaration + name: (identifier) @declaration.name) @declaration.class + +;; Declarations — methods (inside class bodies) +(method_definition + name: (property_identifier) @declaration.name) @declaration.method + +;; Declarations — class fields (JS uses field_definition, not public_field_definition) +(field_definition + property: (property_identifier) @declaration.name) @declaration.property + +;; Declarations — free functions +(function_declaration + name: (identifier) @declaration.name) @declaration.function + +(generator_function_declaration + name: (identifier) @declaration.name) @declaration.function + +;; Arrow / function-expression assigned to a const/let/var. +;; Anchor discipline: @declaration.function sits on the INNER arrow or +;; function_expression, NOT on the lexical_declaration wrapper. This +;; aligns anchor.range with the @scope.function range so +;; pass2AttachDeclarations resolves the innermost scope correctly and +;; resolveCallerGraphId walks up to the right caller anchor. +(lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (arrow_function) @declaration.function)) + +(lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (function_expression) @declaration.function)) + +(export_statement + declaration: (lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (arrow_function) @declaration.function))) + +(export_statement + declaration: (lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (function_expression) @declaration.function))) + +(variable_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (arrow_function) @declaration.function)) + +(variable_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (function_expression) @declaration.function)) + +;; Object-property arrows / function expressions named by their pair key. +;; Same anchor discipline as the lexical_declaration block above: the +;; @declaration.function capture must sit on the INNER arrow/fn-expression. +(pair + key: (property_identifier) @declaration.name + value: (arrow_function) @declaration.function) + +(pair + key: (property_identifier) @declaration.name + value: (function_expression) @declaration.function) + +(pair + key: (string (string_fragment) @declaration.name) + value: (arrow_function) @declaration.function) + +(pair + key: (string (string_fragment) @declaration.name) + value: (function_expression) @declaration.function) + +;; HOC-wrapped variable declarations: const X = HOC((args) => { ... }). +;; Covers React.forwardRef, memo, useCallback, useMemo, observer, +;; debounce, and any user-defined HOC factory. +(lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (call_expression + arguments: (arguments + (arrow_function) @declaration.function)))) + +(lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (call_expression + arguments: (arguments + (function_expression) @declaration.function)))) + +(export_statement + declaration: (lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (call_expression + arguments: (arguments + (arrow_function) @declaration.function))))) + +(export_statement + declaration: (lexical_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (call_expression + arguments: (arguments + (function_expression) @declaration.function))))) + +(variable_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (call_expression + arguments: (arguments + (arrow_function) @declaration.function)))) + +(variable_declaration + (variable_declarator + name: (identifier) @declaration.name + value: (call_expression + arguments: (arguments + (function_expression) @declaration.function)))) + +;; Variable / constant declarations (non-function values). +(lexical_declaration + (variable_declarator + name: (identifier) @declaration.name)) @declaration.const + +(export_statement + declaration: (lexical_declaration + (variable_declarator + name: (identifier) @declaration.name))) @declaration.const + +(variable_declaration + (variable_declarator + name: (identifier) @declaration.name)) @declaration.variable + +;; Imports (ESM) — single anchor per statement; decomposer emits per-specifier markers. +(import_statement) @import.statement + +;; Re-exports with a source clause. +(export_statement + source: (string)) @import.statement + +;; Dynamic imports: import('./m') — tree-sitter-javascript represents this +;; as call_expression with a named import leaf as the function field, +;; identical to tree-sitter-typescript. +(call_expression + function: (import)) @import.dynamic + +;; ── Type bindings (no static annotations in JS; inferred from AST shape) ── + +;; Constructor-inferred: const u = new User() +(variable_declarator + name: (identifier) @type-binding.name + value: (new_expression + constructor: (identifier) @type-binding.type)) @type-binding.constructor + +;; Qualified constructor: const u = new models.User() +(variable_declarator + name: (identifier) @type-binding.name + value: (new_expression + constructor: (member_expression) @type-binding.type)) @type-binding.constructor + +;; Call-result alias: const u = getUser() +(variable_declarator + name: (identifier) @type-binding.name + value: (call_expression + function: (identifier) @type-binding.type)) @type-binding.alias + +;; Member-call alias: const u = svc.getUser() +(variable_declarator + name: (identifier) @type-binding.name + value: (call_expression + function: (member_expression) @type-binding.type)) @type-binding.alias + +;; Await chain: const u = await getUser() / await svc.getUser() +(variable_declarator + name: (identifier) @type-binding.name + value: (await_expression + (call_expression + function: (identifier) @type-binding.type))) @type-binding.alias + +(variable_declarator + name: (identifier) @type-binding.name + value: (await_expression + (call_expression + function: (member_expression) @type-binding.type))) @type-binding.alias + +;; Member-access alias: const addr = user.address +(variable_declarator + name: (identifier) @type-binding.name + value: (member_expression) @type-binding.type) @type-binding.member-alias + +;; Identifier alias: const alias = user +(variable_declarator + name: (identifier) @type-binding.name + value: (identifier) @type-binding.type) @type-binding.alias + +;; Assignment rebind: u = new User() / u = getUser() +(assignment_expression + left: (identifier) @type-binding.name + right: (new_expression + constructor: (identifier) @type-binding.type)) @type-binding.constructor + +(assignment_expression + left: (identifier) @type-binding.name + right: (call_expression + function: (identifier) @type-binding.type)) @type-binding.alias + +(assignment_expression + left: (identifier) @type-binding.name + right: (identifier) @type-binding.type) @type-binding.alias + +;; For-of element: for (const u of users) / for (const u of getUsers()) +(for_in_statement + left: (identifier) @type-binding.name + right: (identifier) @type-binding.type) @type-binding.alias + +(for_in_statement + left: (identifier) @type-binding.name + right: (call_expression + function: (identifier) @type-binding.type)) @type-binding.alias + +(for_in_statement + left: (identifier) @type-binding.name + right: (call_expression + function: (member_expression) @type-binding.type)) @type-binding.alias + +(for_in_statement + left: (identifier) @type-binding.name + right: (member_expression + property: (property_identifier) @type-binding.type)) @type-binding.alias + +;; ── References ──────────────────────────────────────────────────────────── + +;; Free calls: fn(args). The dynamic-import filter runs in captures.ts. +(call_expression + function: (identifier) @reference.name) @reference.call.free + +;; Awaited free call: await fn(...) re-associated by tree-sitter. +(call_expression + function: (await_expression + (identifier) @reference.name)) @reference.call.free + +;; Member calls: obj.method() (includes optional chain). +(call_expression + function: (member_expression + object: (_) @reference.receiver + property: (property_identifier) @reference.name)) @reference.call.member + +;; Awaited member call: await svc.m(...) +(call_expression + function: (await_expression + (member_expression + object: (_) @reference.receiver + property: (property_identifier) @reference.name))) @reference.call.member + +;; Constructor calls: new User() / new ns.User() +(new_expression + constructor: (identifier) @reference.name) @reference.call.constructor + +(new_expression + constructor: (member_expression) @reference.call.constructor.qualified) @reference.call.constructor + +;; Write access: obj.field = value +(assignment_expression + left: (member_expression + object: (_) @reference.receiver + property: (property_identifier) @reference.name)) @reference.write.member + +(augmented_assignment_expression + left: (member_expression + object: (_) @reference.receiver + property: (property_identifier) @reference.name)) @reference.write.member + +;; Read access: obj.field (in read context; captures.ts filters non-reads). +(member_expression + object: (_) @reference.receiver + property: (property_identifier) @reference.name) @reference.read.member +`; + +/** JSX-only suffix — appended when compiling against the JSX grammar for .jsx files. */ +const JSX_QUERY_SUFFIX = ` +;; +((jsx_self_closing_element + name: (identifier) @reference.name) @reference.call.free + (#match? @reference.name "^[A-Z]")) + +;; ... +((jsx_opening_element + name: (identifier) @reference.name) @reference.call.free + (#match? @reference.name "^[A-Z]")) + +;; +(jsx_self_closing_element + name: (member_expression + object: (_) @reference.receiver + property: (property_identifier) @reference.name)) @reference.call.member + +(jsx_opening_element + name: (member_expression + object: (_) @reference.receiver + property: (property_identifier) @reference.name)) @reference.call.member +`; + +let _jsParser: Parser | null = null; +let _jsQuery: Parser.Query | null = null; +let _jsxParser: Parser | null = null; +let _jsxQuery: Parser.Query | null = null; + +export function getJsParser(filePath?: string): Parser { + // JSX files use the same JavaScript grammar in tree-sitter-javascript; + // both .js and .jsx parse with the same grammar object. We keep separate + // singletons only to mirror the TypeScript pattern and in case a future + // version of the grammar diverges. + if (filePath !== undefined && isJsxFile(filePath)) { + if (_jsxParser === null) { + _jsxParser = new Parser(); + _jsxParser.setLanguage(JS_GRAMMAR); + } + return _jsxParser; + } + if (_jsParser === null) { + _jsParser = new Parser(); + _jsParser.setLanguage(JS_GRAMMAR); + } + return _jsParser; +} + +export function getJsScopeQuery(filePath?: string): Parser.Query { + if (filePath !== undefined && isJsxFile(filePath)) { + if (_jsxQuery === null) { + _jsxQuery = new Parser.Query(JS_GRAMMAR, JAVASCRIPT_SCOPE_QUERY + JSX_QUERY_SUFFIX); + } + return _jsxQuery; + } + if (_jsQuery === null) { + _jsQuery = new Parser.Query(JS_GRAMMAR, JAVASCRIPT_SCOPE_QUERY); + } + return _jsQuery; +} + +/** Validate that a cached Tree was produced by the JS grammar. */ +export function jsCachedTreeMatchesGrammar(tree: unknown): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lang = (tree as any)?.getLanguage?.(); + if (lang === undefined || lang === null) return true; + return lang === JS_GRAMMAR; +} diff --git a/gitnexus/src/core/ingestion/languages/javascript/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/javascript/scope-resolver.ts new file mode 100644 index 000000000..1536e793e --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/scope-resolver.ts @@ -0,0 +1,89 @@ +/** + * JavaScript `ScopeResolver` registered in `SCOPE_RESOLVERS` and + * consumed by the generic `runScopeResolution` orchestrator + * (RFC #909 Ring 3, issue #928). + * + * Follows the same minimal wiring-only pattern as TypeScript (the third + * migration). Per-hook logic lives in sibling modules: + * + * - `query.ts` — JS scope query + parser/query singletons + * - `captures.ts` — `emitJsScopeCaptures` (JS grammar, CJS, JSDoc) + * - `interpret.ts` — `interpretJsImport` (delegates to TS interpreter) + * - `simple-hooks.ts` — `jsBindingScopeFor`, `jsImportOwningScope`, + * `jsReceiverBinding` (all delegate to TS hooks) + * - `merge-bindings.ts` — `jsMergeBindings` (delegates to TS function) + * - `arity.ts` — `jsArityCompatibility` (delegates to TS function) + * - `import-target.ts` — `makeJsResolveImportTarget` (TS resolver, JS extensions) + * + * See `./index.ts` for the full per-module rationale. + * + * ## Key differences from TypeScript resolver + * + * - `fieldFallbackOnMethodLookup: true` — JavaScript is dynamically typed; + * the field-fallback heuristic is ENABLED (unlike TypeScript, which + * disables it because the type-binding layer is precise). + * - `allowGlobalFreeCallFallback: true` — CJS `require` patterns and + * global helpers (e.g. `process`, `console`) benefit from workspace- + * wide unique-name fallback. TypeScript uses explicit imports. + * - `loadResolutionConfig` is omitted — JavaScript projects don't use + * `tsconfig.json` path aliases in general. `tsconfigPaths: null` is + * threaded through the resolver adapter. + * - `hoistTypeBindingsToModule: true` — JSDoc `@returns {T}` bindings are + * synthesized on the function scope and hoisted, matching TypeScript's + * method return-type hoisting strategy for cross-file chain resolution. + */ + +import type { ParsedFile } from 'gitnexus-shared'; +import { SupportedLanguages } from 'gitnexus-shared'; +import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js'; +import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js'; +import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js'; +import { javascriptProvider } from '../typescript.js'; +import { jsMergeBindings } from './merge-bindings.js'; +import { jsArityCompatibility } from './arity.js'; +import { makeJsResolveImportTarget } from './import-target.js'; + +const javascriptScopeResolver: ScopeResolver = { + language: SupportedLanguages.JavaScript, + languageProvider: javascriptProvider, + importEdgeReason: 'javascript-scope: import', + + resolveImportTarget: makeJsResolveImportTarget(), + + // JavaScript LEGB — same tier ordering as TypeScript; no declaration- + // merging across type/value/namespace spaces. + mergeBindings: (existing, incoming) => [...jsMergeBindings([...existing, ...incoming])], + + // Adapter: jsArityCompatibility uses (def, callsite); contract is (callsite, def). + arityCompatibility: (callsite, def) => jsArityCompatibility(def, callsite), + + buildMro: (graph, parsedFiles, nodeLookup) => + buildMro(graph, parsedFiles, nodeLookup, defaultLinearize), + + populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed), + + // JavaScript `super` keyword: same pattern as TypeScript. + isSuperReceiver: (text) => /^super(\s*\(|\s*\.|\s*\[|\s*$)/.test(text.trim()), + + // JavaScript is dynamically typed — enable the field-fallback heuristic + // so member-call receivers without type annotations can still resolve + // through declared class fields (e.g. JSDoc-typed fields). + fieldFallbackOnMethodLookup: true, + + // Return-type propagation (across ESM imports) mirrors TypeScript's + // default behavior. JSDoc @returns bindings are hoisted to Module scope + // and propagated to importers via the standard mechanism. + propagatesReturnTypesAcrossImports: true, + + // JSDoc @returns bindings are synthesized on the function/method node + // and hoisted to Module scope by `jsBindingScopeFor` (identical to the + // TypeScript `tsBindingScopeFor` `@type-binding.return` branch). + hoistTypeBindingsToModule: true, + + // CJS-heavy codebases often have utility functions exported without + // explicit imports at the call site. Workspace-wide unique-name fallback + // recovers these edges. + allowGlobalFreeCallFallback: true, +}; + +export { javascriptScopeResolver }; diff --git a/gitnexus/src/core/ingestion/languages/javascript/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/javascript/simple-hooks.ts new file mode 100644 index 000000000..c0947ba5a --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/javascript/simple-hooks.ts @@ -0,0 +1,48 @@ +/** + * Simple hooks for the JavaScript scope-resolution provider. + * + * `jsBindingScopeFor` wraps `tsBindingScopeFor` and adds the JS-only + * `@type-binding.class-field` hoisting rule. The other two hooks + * (`jsImportOwningScope`, `jsReceiverBinding`) are identical to their + * TypeScript counterparts and are re-exported directly. + * + * ## Why class-field hoisting lives here (not in `tsBindingScopeFor`) + * + * `@type-binding.class-field` is emitted exclusively by + * `synthesizeConstructorFieldBindings` in `captures.ts`, which is a + * JavaScript-only synthesis pass. TypeScript uses + * `@type-binding.parameter-property` for constructor parameter + * properties instead. Keeping the JS-only rule in the JS hook file + * prevents language-specific logic from leaking into shared TypeScript + * infrastructure (DoD.md §2.2). + */ + +import type { CaptureMatch, Scope, ScopeId, ScopeTree } from 'gitnexus-shared'; +import { tsBindingScopeFor, walkToScope } from '../typescript/simple-hooks.js'; + +export { + tsImportOwningScope as jsImportOwningScope, + tsReceiverBinding as jsReceiverBinding, +} from '../typescript/simple-hooks.js'; + +/** + * Like `tsBindingScopeFor` but additionally hoists + * `@type-binding.class-field` captures to the enclosing Class scope. + * + * `@type-binding.class-field` is anchored inside the constructor body + * (by `synthesizeConstructorFieldBindings`) so that `walkToScope` can + * walk up from the Function (constructor) scope to the Class scope. + * This puts `User.address → Address` in the class's typeBindings so + * compound-receiver resolution finds it when resolving + * `user.address.save()`. + */ +export function jsBindingScopeFor( + decl: CaptureMatch, + innermost: Scope, + tree: ScopeTree, +): ScopeId | null { + if (decl['@type-binding.class-field'] !== undefined) { + return walkToScope(innermost, tree, 'Class'); + } + return tsBindingScopeFor(decl, innermost, tree); +} diff --git a/gitnexus/src/core/ingestion/languages/typescript.ts b/gitnexus/src/core/ingestion/languages/typescript.ts index c3fad3518..f597396b6 100644 --- a/gitnexus/src/core/ingestion/languages/typescript.ts +++ b/gitnexus/src/core/ingestion/languages/typescript.ts @@ -56,6 +56,16 @@ import { typescriptArityCompatibility, resolveTsImportTarget, } from './typescript/index.js'; +import { + emitJsScopeCaptures, + interpretJsImport, + interpretJsTypeBinding, + jsBindingScopeFor, + jsImportOwningScope, + jsReceiverBinding, + jsMergeBindings, + jsArityCompatibility, +} from './javascript/index.js'; /** * TypeScript/JavaScript: arrow_function and function_expression are @@ -359,4 +369,19 @@ export const javascriptProvider = defineLanguage({ classExtractor: createClassExtractor(javascriptClassConfig), heritageExtractor: createHeritageExtractor(SupportedLanguages.JavaScript), builtInNames: BUILT_INS, + + // ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ────────── + // JavaScript is the fourth migration after Python, C#, and TypeScript. + // Hooks are thin wrappers over the TypeScript implementations where + // semantics are identical; JS-specific additions (CJS require(), + // JSDoc type bindings) live in ./javascript/captures.ts. + // See ./javascript/index.ts for the full per-module rationale. + emitScopeCaptures: emitJsScopeCaptures, + interpretImport: interpretJsImport, + interpretTypeBinding: interpretJsTypeBinding, + bindingScopeFor: jsBindingScopeFor, + importOwningScope: jsImportOwningScope, + mergeBindings: (_scope, bindings) => jsMergeBindings(bindings), + receiverBinding: jsReceiverBinding, + arityCompatibility: jsArityCompatibility, }); diff --git a/gitnexus/src/core/ingestion/languages/typescript/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/typescript/simple-hooks.ts index 1dbb03dc7..f8f7302fa 100644 --- a/gitnexus/src/core/ingestion/languages/typescript/simple-hooks.ts +++ b/gitnexus/src/core/ingestion/languages/typescript/simple-hooks.ts @@ -75,8 +75,11 @@ export function tsBindingScopeFor( * any of `kinds`. Returns the matching scope's id or `null` when no * ancestor matches (e.g., a return type binding emitted outside any * Module scope — shouldn't happen in well-formed input). + * + * Exported so language-specific hook wrappers (e.g. `jsBindingScopeFor`) + * can reuse it without duplicating the traversal logic. */ -function walkToScope( +export function walkToScope( from: Scope, tree: ScopeTree, ...kinds: readonly Scope['kind'][] diff --git a/gitnexus/src/core/ingestion/registry-primary-flag.ts b/gitnexus/src/core/ingestion/registry-primary-flag.ts index 869157adb..e050c6553 100644 --- a/gitnexus/src/core/ingestion/registry-primary-flag.ts +++ b/gitnexus/src/core/ingestion/registry-primary-flag.ts @@ -74,6 +74,7 @@ export const MIGRATED_LANGUAGES: ReadonlySet = new Set = n [SupportedLanguages.C, cScopeResolver], [SupportedLanguages.CPlusPlus, cppScopeResolver], [SupportedLanguages.PHP, phpScopeResolver], + [SupportedLanguages.JavaScript, javascriptScopeResolver], ]); diff --git a/gitnexus/test/fixtures/cross-file-binding/js-consumer-before-provider/b-provider.js b/gitnexus/test/fixtures/cross-file-binding/js-consumer-before-provider/b-provider.js index 99c85948a..b04e7c84a 100644 --- a/gitnexus/test/fixtures/cross-file-binding/js-consumer-before-provider/b-provider.js +++ b/gitnexus/test/fixtures/cross-file-binding/js-consumer-before-provider/b-provider.js @@ -2,6 +2,7 @@ export class User { save() {} } +/** @returns {User} */ export function getUser() { return new User(); } diff --git a/gitnexus/test/fixtures/cross-file-binding/js-cross-file/src/models.js b/gitnexus/test/fixtures/cross-file-binding/js-cross-file/src/models.js index 5d33e9be4..59bb80058 100644 --- a/gitnexus/test/fixtures/cross-file-binding/js-cross-file/src/models.js +++ b/gitnexus/test/fixtures/cross-file-binding/js-cross-file/src/models.js @@ -3,6 +3,7 @@ export class User { getName() { return ''; } } +/** @returns {User} */ export function getUser() { return new User(); } From 73543a471449f468bb9b831c053677b9651087fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:54:14 +0100 Subject: [PATCH 2/7] chore(deps)(deps-dev): bump @types/node in /gitnexus (#1696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.2 to 25.7.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.7.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar --- gitnexus/package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index fe2115d07..1e5bb9a39 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -2065,12 +2065,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", - "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": "~7.21.0" } }, "node_modules/@types/qs": { @@ -5551,9 +5551,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "license": "MIT" }, "node_modules/universalify": { From 637cfca39ce2054956fe7a10a7d546e6a98c06d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:54:33 +0100 Subject: [PATCH 3/7] chore(deps)(deps): bump express-rate-limit in /gitnexus (#1697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.5.1 to 8.5.2. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.5.1...v8.5.2) --- updated-dependencies: - dependency-name: express-rate-limit dependency-version: 8.5.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar Co-authored-by: Abhigyan Patwari <126312502+abhigyanpatwari@users.noreply.github.com> --- gitnexus/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 1e5bb9a39..1881b8ab0 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -3059,9 +3059,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", - "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { "ip-address": "^10.2.0" From 6210d80f1e790640848baaefc6144a9d99096c6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:55:04 +0100 Subject: [PATCH 4/7] chore(deps)(deps-dev): bump tsx from 4.21.0 to 4.21.1 in /gitnexus (#1698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tsx](https://github.com/privatenumber/tsx) from 4.21.0 to 4.21.1. - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.21.0...v4.21.1) --- updated-dependencies: - dependency-name: tsx dependency-version: 4.21.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar Co-authored-by: Abhigyan Patwari <126312502+abhigyanpatwari@users.noreply.github.com> --- gitnexus/package-lock.json | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 1881b8ab0..253b770d5 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -3284,19 +3284,6 @@ "node": ">= 0.4" } }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/gitnexus-shared": { "resolved": "../gitnexus-shared", "link": true @@ -4692,16 +4679,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/rolldown": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", @@ -5492,14 +5469,13 @@ "optional": true }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.1.tgz", + "integrity": "sha512-5QE2Q04cN1u0993w0LT5rPw3faZqZU1fFn1mGE0pV53N1Dn7c+QFFxQu1mBeSgeOXwFyTicZw02wVgp3Tb5cAQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.27.0" }, "bin": { "tsx": "dist/cli.mjs" From 15a667ae5ec77ca261902561e8574664c119a73a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:55:24 +0100 Subject: [PATCH 5/7] chore(deps)(deps-dev): bump @vitest/coverage-v8 in /gitnexus (#1604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 4.1.5 to 4.1.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.6/packages/coverage-v8) --- updated-dependencies: - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar Co-authored-by: Abhigyan Patwari <126312502+abhigyanpatwari@users.noreply.github.com> From c746f30c909b9f6c3ab9ebdcc2949ddec7fe618b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:55:44 +0100 Subject: [PATCH 6/7] chore(deps)(deps): bump langsmith (#1552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the npm_and_yarn group with 1 update in the /gitnexus-web directory: [langsmith](https://github.com/langchain-ai/langsmith-sdk). Updates `langsmith` from 0.5.23 to 0.6.3 - [Release notes](https://github.com/langchain-ai/langsmith-sdk/releases) - [Commits](https://github.com/langchain-ai/langsmith-sdk/commits) --- updated-dependencies: - dependency-name: langsmith dependency-version: 0.6.3 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar Co-authored-by: Abhigyan Patwari <126312502+abhigyanpatwari@users.noreply.github.com> --- gitnexus-web/package-lock.json | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index cf69e1083..6cf5bcf2d 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -5599,13 +5599,12 @@ } }, "node_modules/langsmith": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.23.tgz", - "integrity": "sha512-dE/M/2Gg2S2R8ygDdkWGJVO3JstijvsNvPXsy9V8WGbpb88Zn8xF/aTjPx4mIy5gIoo02T6FssOgYyLf51Dv1Q==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.6.3.tgz", + "integrity": "sha512-pXrQ4/4myQvjFFOAUmt5pWRrLEZR20gzIJD7MNdUH+5/S5nLI4ZRBo/SYKC6coaYj9pYTfQdBIzcs+3kfJ5uDA==", "license": "MIT", "dependencies": { - "p-queue": "6.6.2", - "uuid": "10.0.0" + "p-queue": "6.6.2" }, "peerDependencies": { "@opentelemetry/api": "*", @@ -5632,19 +5631,6 @@ } } }, - "node_modules/langsmith/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", From dad1ca7ab55ee3dae13729e4cbbce54b8c070f15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 06:56:04 +0100 Subject: [PATCH 7/7] chore(deps)(deps): bump zod from 3.25.76 to 4.3.6 in /gitnexus-web (#1464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [zod](https://github.com/colinhacks/zod) from 3.25.76 to 4.3.6. - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v3.25.76...v4.3.6) --- updated-dependencies: - dependency-name: zod dependency-version: 4.3.6 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar Co-authored-by: Abhigyan Patwari <126312502+abhigyanpatwari@users.noreply.github.com> --- gitnexus-web/package-lock.json | 8 ++++---- gitnexus-web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index 6cf5bcf2d..6297ec5c5 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -41,7 +41,7 @@ "sigma": "^3.0.2", "tailwindcss": "^4.2.4", "uuid": "^14.0.0", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@babel/types": "^7.29.0", @@ -8889,9 +8889,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 211bb65c7..e93dc82c2 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -51,7 +51,7 @@ "sigma": "^3.0.2", "tailwindcss": "^4.2.4", "uuid": "^14.0.0", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@babel/types": "^7.29.0",