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 0494b0746..46960b967 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,7 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING. # GitNexus — Code Intelligence -This project is indexed by GitNexus as **gitnexus** (18208 symbols, 25369 relationships, 300 execution flows). Use the GitNexus 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 GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. @@ -74,19 +74,6 @@ This project is indexed by GitNexus as **gitnexus** (18208 symbols, 25369 relati - 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"})`. -## When Debugging - -1. `gitnexus_query({query: ""})` — find execution flows related to the issue -2. `gitnexus_context({name: ""})` — see all callers, callees, and process participation -3. `READ gitnexus://repo/gitnexus/process/{processName}` — trace the full execution flow step by step -4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed - -## When Refactoring - -- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`. -- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code. -- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed. - ## Never Do - NEVER edit a function, class, or method without first running `gitnexus_impact` on it. @@ -94,81 +81,14 @@ This project is indexed by GitNexus as **gitnexus** (18208 symbols, 25369 relati - 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. -## Tools Quick Reference - -| Tool | When to use | Command | -|------|-------------|---------| -| `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 these | -| d=2 | LIKELY AFFECTED — indirect deps | Should test | -| d=3 | MAY NEED TESTING — transitive | Test if critical path | - ## 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 - -Before completing any code modification task, verify: -1. `gitnexus_impact` was run for all modified symbols -2. No HIGH/CRITICAL risk warnings were ignored -3. `gitnexus_detect_changes()` confirms changes match expected scope -4. All d=1 (WILL BREAK) dependents were updated - -## Keeping the Index Fresh - -After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it: - -```bash -npx gitnexus analyze -``` - -If the index previously included embeddings, preserve them by adding `--embeddings`: - -```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 @@ -180,20 +100,26 @@ Check `.gitnexus/meta.json` `stats.embeddings` (0 = none). A plain `analyze` no | 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` | - -## 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. | +| 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 04150725d..70b3f1a36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,4 +51,68 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g ## GitNexus rules -See the canonical `gitnexus:start` … `gitnexus:end` block in [AGENTS.md](AGENTS.md). +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-web/package-lock.json b/gitnexus-web/package-lock.json index cf69e1083..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", @@ -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", @@ -8903,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", diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 4e6a4b63b..cfa743483 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -17,7 +17,7 @@ "cli-progress": "^3.12.0", "commander": "^14.0.3", "cors": "^2.8.5", - "express": "^4.19.2", + "express": "^5.2.1", "express-rate-limit": "^8.4.1", "glob": "^13.0.6", "graphology": "^0.26.0", @@ -52,7 +52,7 @@ "devDependencies": { "@types/cli-progress": "^3.11.6", "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", + "@types/express": "^5.0.6", "@types/js-yaml": "^4.0.9", "@types/node": "^25.6.0", "@types/uuid": "^11.0.0", @@ -1290,268 +1290,6 @@ } } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -2018,22 +1756,21 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", "dependencies": { @@ -2057,26 +1794,19 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "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": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "dev": true, "license": "MIT" }, @@ -2098,25 +1828,13 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -2276,13 +1994,13 @@ } }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" @@ -2360,12 +2078,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2407,63 +2119,33 @@ } }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" + "node": ">=18" }, - "engines": { - "node": ">= 0.8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2612,15 +2294,16 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -2649,10 +2332,13 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/cors": { "version": "2.8.6", @@ -2784,16 +2470,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3013,45 +2689,42 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", @@ -3059,9 +2732,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", - "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", + "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.1.0" @@ -3076,21 +2749,6 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/fast-copy": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", @@ -3144,38 +2802,26 @@ } }, "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/flatbuffers": { "version": "25.9.23", "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", @@ -3192,12 +2838,12 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs-extra": { @@ -3284,19 +2930,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 @@ -3512,15 +3145,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -4031,63 +3668,49 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/minimatch": { @@ -4170,9 +3793,9 @@ } }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -4376,12 +3999,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4634,22 +4251,6 @@ "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -4692,16 +4293,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", @@ -4762,26 +4353,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -4826,44 +4397,31 @@ } }, "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/serialize-error": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", @@ -4880,18 +4438,22 @@ } }, "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -5498,14 +5060,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" @@ -5530,16 +5091,34 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typescript": { @@ -5557,9 +5136,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": { @@ -5586,15 +5165,6 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/uuid": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index 7447961ba..3863a0457 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -60,7 +60,7 @@ "cli-progress": "^3.12.0", "commander": "^14.0.3", "cors": "^2.8.5", - "express": "^4.19.2", + "express": "^5.2.1", "express-rate-limit": "^8.4.1", "glob": "^13.0.6", "graphology": "^0.26.0", @@ -100,7 +100,7 @@ "devDependencies": { "@types/cli-progress": "^3.11.6", "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", + "@types/express": "^5.0.6", "@types/js-yaml": "^4.0.9", "@types/node": "^25.6.0", "@types/uuid": "^11.0.0", 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/src/server/api.ts b/gitnexus/src/server/api.ts index d1aedbd05..a55ce7bec 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -451,7 +451,14 @@ export const streamGraphNdjson = async ( */ const mountSSEProgress = (app: express.Express, routePath: string, jm: JobManager) => { app.get(routePath, (req, res) => { - const job = jm.getJob(req.params.jobId); + let jobId: string; + try { + jobId = assertString(req.params.jobId, 'jobId'); + } catch (err: any) { + res.status(err.status ?? 400).json({ error: err.message }); + return; + } + const job = jm.getJob(jobId); if (!job) { res.status(404).json({ error: 'Job not found' }); return; @@ -497,7 +504,7 @@ const mountSSEProgress = (app: express.Express, routePath: string, jm: JobManage try { eventId++; if (progress.phase === 'complete' || progress.phase === 'failed') { - const eventJob = jm.getJob(req.params.jobId); + const eventJob = jm.getJob(jobId); res.write( `id: ${eventId}\nevent: ${progress.phase}\ndata: ${JSON.stringify({ repoName: eventJob?.repoName, 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(); }