diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts index 94ab2168c..2deac2a96 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/unique-name-properties.ts @@ -69,6 +69,7 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe import type { GraphNodeLookup } from '../graph-bridge/node-lookup.js'; import { resolveCallerGraphId } from '../graph-bridge/ids.js'; import { callableFlowSiteKey } from './callable-value-flow.js'; +import { isTestFile } from '../../entry-point-scoring.js'; import { getLanguageFromFilename } from 'gitnexus-shared'; /** Language a definition lives in, for reporting which anchor a reader cannot reach. */ @@ -310,6 +311,21 @@ function narrowToSingleCandidate( // what guarantees R3-4 cannot change an answer that already resolved before // return shapes were indexed at all. let candidates = candidatesIn; + + // PRODUCTION CODE FIRST. A test constructs throwaway shapes with the same + // field names as the thing it exercises — measured on the reporting repo, + // four of the seven JavaScript anchors for `wickRatio` are in `tests/` — and + // a read in production code cannot mean any of them. Applied before the + // declared/return-shape split because "is this the shipped program" is the + // stronger signal: a declaration in a test fixture is still a test fixture. + // + // Only when the READER is production. A read inside a test legitimately means + // the test's own shape, so this must not fire there. + if (!isTestFile(readingFile)) { + const production = candidates.filter((c) => !isTestFile(c.filePath)); + if (production.length > 0) candidates = production; + } + const declared = candidates.filter((c) => !c.fromReturnShape); const ranked = declared.length > 0 ? declared : candidates; diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/fixture-shapes.test.js b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/fixture-shapes.test.js new file mode 100644 index 000000000..ebf8a73fe --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/fixture-shapes.test.js @@ -0,0 +1,9 @@ +// A TEST file that constructs a throwaway shape with a production field name. +// Measured on the reporting repo: four of seven JavaScript anchors for one +// field lived in `tests/`, competing with the three real ones and making every +// production read ambiguous. A read in production cannot mean any of these. +export function buildTestFixture() { + return { + productionAndTestField: 'fixture', + }; +} diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/production-reader.js b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/production-reader.js new file mode 100644 index 000000000..6c7638059 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/production-reader.js @@ -0,0 +1,8 @@ +// The reader lives in its OWN file and imports neither anchor, so no same-file +// or direct-import tier can decide this. What is left is production-vs-test, +// which is exactly the tier under test — with a reader beside the production +// anchor, the same-file tier resolves it either way and the assertion proves +// nothing. +export function readsProductionShape(bag) { + return bag.productionAndTestField; +} diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js index da8e5a2af..cc7e49877 100644 --- a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js +++ b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js @@ -45,3 +45,11 @@ export const anonHolder = [ return { anonReturnKey: row.x }; }, ]; + +// The production anchor for a name a test fixture also constructs. A read here +// must resolve to THIS one, not to the fixture's. +export function buildProductionShape(row) { + return { + productionAndTestField: row.real, + }; +} diff --git a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts index e7a72877e..56ba7a455 100644 --- a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts +++ b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts @@ -203,6 +203,25 @@ describe('JavaScript plain-object property access (A1/A5)', () => { expect(ids.some((id) => id.includes('formatSummary.summaryOnlyField'))).toBe(true); }); + // Production code outranks test fixtures. Measured on the reporting repo: + // four of the seven JavaScript anchors for one field were in `tests/`, + // competing with the three real ones and making every production read + // ambiguous. A test builds throwaway shapes with production field names; a + // read in shipped code cannot mean one. + it('does not let a test fixture compete with the production anchor', () => { + expect(readersOf('productionAndTestField')).toContain('readsProductionShape'); + const ids = Array.from( + (result as unknown as { graph: { iterNodes(): Iterable } }).graph.iterNodes(), + ) + .filter((n) => n.label === 'Property') + .map((n) => String(n.id)); + // Both anchors exist — it is the RANKING that differs, not the indexing. + expect(ids.some((id) => id.includes('buildProductionShape.productionAndTestField'))).toBe( + true, + ); + expect(ids.some((id) => id.includes('buildTestFixture.productionAndTestField'))).toBe(true); + }); + // THE GUARANTEE that reconciles this with R2-1b. `sharedWithDeclared` is // both a named-object key and a return-shape key; a read must still resolve // to the DECLARED one, or indexing return shapes would silently move