From 85800fc4e20ee281f40563a8926abd1e9f303de2 Mon Sep 17 00:00:00 2001 From: ReidenXerx Date: Thu, 6 Aug 2026 21:13:40 +0300 Subject: [PATCH] feat(scope-resolution): capture record construction as property writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The read side answered well after the narrowing work while "who SETS this field?" still missed the code that stamps the value. A record built inline — `return { exitContract: { exitMinAtrMult: settings.x } }` — is bound to no variable, so it minted no definition and its keys referenced nothing. Modelled as WRITE REFERENCES, deliberately not definitions. 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 fields resolvable — measured at 26 competing definitions for one field on the reporting repo, which is what made every backend read unanswerable in the first place. 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 — and is asserted as such. The enclosing literal is the receiver and it is anonymous, so these route through the same narrowing and the same refusal-to-guess as every other untyped receiver. Verified on the reporting repo: `entryPlan.js` went from no rows to `selectExitEnvelope` as a writer of `exitMinAtrMult`. Both captures mutation-checked. Co-Authored-By: Claude Opus 5 (1M context) --- .../ingestion/languages/javascript/query.ts | 33 +++++++++++++++++++ .../destructured.js | 25 ++++++++++++++ .../javascript-object-properties.test.ts | 30 +++++++++++++++++ 3 files changed, 88 insertions(+) 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