diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts index ec2d6a3f2..d7fa30fc3 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/query.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -589,6 +589,17 @@ export const JAVASCRIPT_SCOPE_QUERY = ` (object (shorthand_property_identifier) @reference.name @reference.property-key @reference.value-ref) + +;; Bare-identifier reads (A2). VALUE POSITIONS ONLY — a blanket +;; \`(identifier)\` rule would mint a site for every token in the file. +(arguments + (identifier) @reference.name @reference.read.identifier) + +(assignment_pattern + right: (identifier) @reference.name @reference.read.identifier) + +(return_statement + (identifier) @reference.name @reference.read.identifier) `; /** JSX-only suffix — appended when compiling against the JSX grammar for .jsx files. */ diff --git a/gitnexus/src/storage/parse-cache.ts b/gitnexus/src/storage/parse-cache.ts index 63654ffb8..90cc24b4f 100644 --- a/gitnexus/src/storage/parse-cache.ts +++ b/gitnexus/src/storage/parse-cache.ts @@ -248,7 +248,25 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j // them. Only comparing against origin/main at MERGE time surfaces it. // PR #2840 (Objective-C, draft) still claims 44 as well — it must move too. // RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING. -const SCHEMA_BUMP = 45; +// 45 -> 46 for the JavaScript bare-identifier read captures (A2), which emit +// `@reference.read.identifier` in value positions (call arguments, +// default-parameter values, return statements) so a module-scope `const` read +// only by bare name finally mints a reference site, plus the object-literal +// `@definition.property` rule and the TypeScript shape-member captures. All +// PARSE-TIME emission, so a warm cache serves entries carrying none of those +// matches and the new nodes and edges never appear — observed directly while +// developing: a full `analyze --force` produced a byte-identical graph and read +// as a failed hypothesis until the cache was cleared by hand. +// +// This branch originally took 45 and it COLLIDED: #2837 above merged first and +// claimed it. The TENTH entry in this ledger and the FOURTH exact clash, caught +// exactly as the note above says it must be — by comparing against origin/main +// at merge time, not at review time. The pin test cannot catch it: both sides +// asserted `toBe(45)`, which passes while main is already 45, so two capture +// schemas would have shared one PARSE_CACHE_VERSION and the durable ParsedFile +// store would have replayed pre-fix ParsedFiles verbatim for one of them. +// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING. +const SCHEMA_BUMP = 46; const GITNEXUS_PKG_VERSION = (() => { try { // package.json sits at gitnexus/package.json — two levels up from diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-const-references/config.js b/gitnexus/test/fixtures/lang-resolution/javascript-const-references/config.js new file mode 100644 index 000000000..2e5041c5e --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/javascript-const-references/config.js @@ -0,0 +1,15 @@ +// A2: a module-scope const referenced only as a bare identifier. +const DEFAULT_FETCH_LIMIT = 500; + +// Inline-exported form — the common spelling. +export const INLINE_LIMIT = 250; + +export function fetchAll(limit = DEFAULT_FETCH_LIMIT) { + return Math.max(DEFAULT_FETCH_LIMIT, limit); +} + +export function pageSize() { + return DEFAULT_FETCH_LIMIT; +} + +export { DEFAULT_FETCH_LIMIT }; diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-const-references/consumer.js b/gitnexus/test/fixtures/lang-resolution/javascript-const-references/consumer.js new file mode 100644 index 000000000..ddc7cd685 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/javascript-const-references/consumer.js @@ -0,0 +1,16 @@ +import { DEFAULT_FETCH_LIMIT, INLINE_LIMIT, pageSize } from './config.js'; + +// A2 cross-file: a named-import reference to a module-scope const. +export function consumerLimit() { + return DEFAULT_FETCH_LIMIT; +} + +// Control: same shape, but the const was exported inline. +export function consumerInline() { + return INLINE_LIMIT; +} + +// Control: a cross-file CALL through the same import statement resolves today. +export function consumerCall() { + return pageSize(); +} diff --git a/gitnexus/test/integration/resolvers/javascript-const-references.test.ts b/gitnexus/test/integration/resolvers/javascript-const-references.test.ts new file mode 100644 index 000000000..574c8aaa3 --- /dev/null +++ b/gitnexus/test/integration/resolvers/javascript-const-references.test.ts @@ -0,0 +1,64 @@ +/** + * A2 — references to a module-scope `const` must produce edges. + * + * A constant read only as a BARE IDENTIFIER (`Math.max(LIMIT, n)`, a default + * parameter value, `return LIMIT`) produced no reference site at all, so + * "who uses this constant?" — the question behind every dead-code trim and + * constants refactor — answered with a confident zero rather than "unknown". + * + * The registries already accept it (`FIELD_KINDS` includes `Const`) and the + * scope query already declares it (`@declaration.const`), so this is about the + * reference SITE existing: JS/TS captured only `@reference.read.member`, which + * requires a receiver a bare identifier does not have. + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import path from 'path'; +import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js'; + +describe('JavaScript module-scope const references (A2)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo( + path.join(FIXTURES, 'javascript-const-references'), + () => {}, + ); + }, 60000); + + const readersOfConst = (): Set => + new Set( + getRelationships(result, 'ACCESSES') + .filter((e) => e.target === 'DEFAULT_FETCH_LIMIT') + .map((e) => e.source), + ); + + it('emits ACCESSES from same-file readers of the const', () => { + const readers = readersOfConst(); + // fetchAll reads it twice (default param + Math.max); pageSize returns it. + expect(readers).toContain('fetchAll'); + expect(readers).toContain('pageSize'); + }); + + // Cross-file is NOT yet covered. The reference site exists (the capture + // fires on `return DEFAULT_FETCH_LIMIT` in consumer.js, verified against the + // raw query) and a CALL through the very same import statement resolves + // (`consumerCall → pageSize`, reason `import-resolved`), so the gap is + // specific to linking a value-kind def across the import edge. Prime + // suspect: exported-def resolution is callable-only — `findExportedDefByName` + // returns a def only when `def.type` is `Function`/`Method` + // (scope/walkers.ts:1323) and its workspace fallback index is + // `exportedCallableByName`. Both export spellings (`export const X` and + // `export { X }`) fail identically, so it is not the export syntax. + it.todo('emits an edge for the cross-file named-import reader'); + + it('targets the Const node itself, not a same-named local', () => { + const toConst = getRelationships(result, 'ACCESSES').filter( + (e) => e.target === 'DEFAULT_FETCH_LIMIT', + ); + expect(toConst.length).toBeGreaterThan(0); + for (const e of toConst) { + expect(e.targetLabel).toBe('Const'); + expect(e.targetFilePath).toContain('config.js'); + } + }); +});