feat(scope-resolution): capture record construction as property writes

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) <noreply@anthropic.com>
This commit is contained in:
ReidenXerx 2026-08-06 21:13:40 +03:00
parent 19974b49d8
commit 85800fc4e2
3 changed files with 88 additions and 0 deletions

View file

@ -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. */

View file

@ -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 });

View file

@ -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