diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts index 5b0b5cb1a..c2db3a8c7 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/query.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -647,6 +647,39 @@ export const JAVASCRIPT_SCOPE_QUERY = ` key: (property_identifier) @reference.name @reference.read.destructured)) @reference.receiver) +;; Object-literal keys in RECORD CONSTRUCTION position (R2-1b). Building +;; \`{ exitContract: { exitMinAtrMult: settings.x } }\` SETS that field, so this +;; is the write counterpart to the destructured read above — without it +;; "who reads this setting?" answers well and "who SETS it?" misses the code +;; that stamps the value. +;; +;; A WRITE REFERENCE, deliberately not a definition. The round-1 rule already +;; mints Property nodes for literals bound to a variable; minting more for +;; anonymous records would add same-named competitors to the very name-narrowing +;; that makes these reads resolvable — measured at 26 competing definitions for +;; one field on the reporting repo. A construction site is a USE of a field, not +;; another declaration of it. +;; +;; Two positions only: nested under a key, and returned. Both are records with a +;; name attached (the key, or the function). An inline call argument +;; (\`doThing({ id: 1 })\`) stays excluded for the same reason round 1 excluded +;; it from definitions — it is call-site data, not a named surface. +;; +;; The enclosing literal is the receiver, and it is anonymous, which routes +;; these through the same narrowing and the same refusal-to-guess as every other +;; untyped receiver. +(pair + value: (object + (pair + key: (property_identifier) @reference.name + @reference.write.property-key) @_r2b.nested) @reference.receiver) + +(return_statement + (object + (pair + key: (property_identifier) @reference.name + @reference.write.property-key) @_r2b.returned) @reference.receiver) + `; /** JSX-only suffix — appended when compiling against the JSX grammar for .jsx files. */ diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js index 17e3c2f7c..90f19867b 100644 --- a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js +++ b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js @@ -17,3 +17,28 @@ export function appliesRenamed({ destructuredOnlyField: aliased }) { export function appliesShorthand({ destructuredOnlyField }) { return destructuredOnlyField; } + +// R2-1b: record CONSTRUCTION. Both of these SET `destructuredOnlyField` — +// the nested-under-a-key form and the returned-literal form — and neither is +// bound to a variable, so neither mints a definition. +export function buildPlan(settings) { + return { + exitContract: { + destructuredOnlyField: settings.raw ?? 0, + }, + }; +} + +export function buildFlat(settings) { + return { + destructuredOnlyField: settings.raw ?? 1, + }; +} + +// NEGATIVE CONTROL: an inline call-argument prop bag is call-site data, not a +// record with a name attached, and must stay excluded exactly as it is for +// definitions. +function consume(bag) { + return bag; +} +export const consumed = consume({ notAConstructedField: 3 }); diff --git a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts index 629e2f44a..787133bca 100644 --- a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts +++ b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts @@ -124,6 +124,36 @@ describe('JavaScript plain-object property access (A1/A5)', () => { }); }); + // R2-1b. The read side answered well while "who SETS this field?" missed the + // code that stamps the value, because a record built inline is bound to no + // variable and so mints no definition to point at. + describe('record construction writes (R2-1b)', () => { + const writersOf = (field: string): string[] => + getRelationships(result, 'ACCESSES') + .filter((e) => e.target === field && (e.rel.reason ?? '').includes('write')) + .map((e) => e.source); + + it('emits a WRITE for a literal nested under a key', () => { + expect(writersOf('destructuredOnlyField')).toContain('buildPlan'); + }); + + it('emits a WRITE for a returned literal', () => { + expect(writersOf('destructuredOnlyField')).toContain('buildFlat'); + }); + + // The point of modelling these as writes rather than definitions: more + // definitions would add same-named competitors to the narrowing that makes + // these fields resolvable in the first place. + it('mints NO new definition for a constructed record', () => { + expect(propertyNames().filter((n) => n === 'destructuredOnlyField')).toHaveLength(1); + }); + + it('leaves an inline call-argument prop bag alone', () => { + expect(propertyNames()).not.toContain('notAConstructedField'); + expect(writersOf('notAConstructedField')).toEqual([]); + }); + }); + // R2. Strict workspace uniqueness was measurably too blunt: in the reporting // repo `exitMinAtrMult` had 26 definitions, 16 of them in one-off scripts the // backend has no relationship with, so every backend read was refused because