candor-ts 0.40.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/policy.mjs +8 -0
  3. package/scan.mjs +961 -88
package/scan.mjs CHANGED
@@ -40,6 +40,14 @@ import { isTestPath, kappa, kappaKnows, nodeCoreUnreviewed, fsKind, commandHeadE
40
40
  WEB_WIRE_MEMBERS, CONNECTING_CTORS, NET_ESTABLISHING, NET_ACCEPTING, NET_REQUEST_NAMED, FS_USE_VERBS,
41
41
  EXEC_USE_VERBS, RESERVED_SIDECAR_SEGMENTS } from "./scan-core.mjs";
42
42
  import { emitSurface } from "./surface.mjs";
43
+ // ⟨R815⟩ reach marker for the class-definition-time attribution in `enclosing` (declared here, not beside it:
44
+ // `enclosing` runs from module-level code above its own definition, so a later `const` is in its TDZ).
45
+ const R815_REACH = process.env.CANDOR_R815_REACH ? (k) => console.error(`R815-REACH ${k}`) : null;
46
+ // ⟨R934/R935⟩ reach marker: a charge that came from a PARAMETER or PROPERTY environment location.
47
+ const R246_REACH = process.env.CANDOR_R246_REACH ? (k) => console.error(`R246-REACH ${k}`) : null;
48
+ let projAccessorNames = null;
49
+ const anyRecvSetResidue = []; // ⟨R246/R1039⟩ { owner, name|null, kind } — see the post-fixpoint disclosure
50
+ const ENVLOC_REACH = process.env.CANDOR_ENVLOC_REACH ? (k) => console.error(`ENVLOC-REACH ${k}`) : null;
43
51
 
44
52
  const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
45
53
 
@@ -2655,7 +2663,16 @@ function declModule(decl) {
2655
2663
  let m = f.match(/@types\/node\/(.+?)\.d\.ts$/);
2656
2664
  if (m) return m[1];
2657
2665
  if (/typescript\/lib\/lib\..*\.d\.ts$/.test(f)) return "<es-lib>";
2658
- m = f.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
2666
+ // ⟨SOUNDNESS R778⟩ The INNERMOST `node_modules/<pkg>/` owns the file, not the first. pnpm's real path is
2667
+ // `node_modules/.pnpm/fs-extra@11.2.0/node_modules/fs-extra/…` and an npm nested install is
2668
+ // `node_modules/outer/node_modules/inner/…`; the first-segment match keyed every foreign package of a pnpm
2669
+ // tree as `.pnpm` and `inner` as `outer`. MEASURED with the installer the only variable (fx/r778-{npm,pnpm},
2670
+ // EXECUTED to write the file): npm read `['Fs']` and `deny Fs` exit 1, pnpm read `[]` + `invisible:['.pnpm']`
2671
+ // and exit 0 — a FALSE disclosure (`.pnpm` is not a package) hiding a κ-classified effect. For a flat npm
2672
+ // layout the first and last segments are the same one, so no npm key moves. A file directly under a dot-store
2673
+ // with no inner `node_modules/` (no package segment at all) asks the nearest `package.json` instead.
2674
+ m = [...f.matchAll(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//g)].at(-1) ?? null;
2675
+ if (m && m[1].startsWith(".")) m = (() => { const n = nearestPackageName(f); return n ? [null, n] : m; })();
2659
2676
  if (m) {
2660
2677
  // `@types/X` (DefinitelyTyped) provides types for the RUNTIME package X — map it to X so the curated κ
2661
2678
  // tier (keyed by the runtime name: pg/ws/…) fires. Without this a package typed via @types resolved to
@@ -3461,8 +3478,7 @@ function literalHeadHostUrl(expr) {
3461
3478
  // (an assignment rooted at it, another call's argument, an alias, a return) refuses the capture.
3462
3479
  function determinedUrlObject(expr, call, depth = 0) {
3463
3480
  if (!expr || depth > 1) return null;
3464
- while (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isNonNullExpression(expr)
3465
- || ts.isSatisfiesExpression?.(expr)) expr = expr.expression;
3481
+ expr = unwrapArgExpr(expr); // R780: the one transparent-wrapper set
3466
3482
  if (ts.isNewExpression(expr) && ts.isIdentifier(expr.expression)
3467
3483
  && (expr.expression.text === "URL" || expr.expression.text === "Request")) {
3468
3484
  const decls = checker.getSymbolAtLocation(expr.expression)?.declarations ?? [];
@@ -3516,8 +3532,17 @@ function determinedUrlObject(expr, call, depth = 0) {
3516
3532
  visit(d.getSourceFile());
3517
3533
  return safe ? determinedUrlObject(d.initializer, call, depth + 1) : null;
3518
3534
  }
3519
- // R947 — the invoked function's own ARGUMENTS on the reflective routes, or `null` when they are not
3520
- // visible here (a spread, an `.apply` of a non-literal array). `null` is the safe answer: it captures nothing.
3535
+ // ⟨SOUNDNESS R780⟩ THE ONE TRANSPARENT-WRAPPER SET. Parentheses, `as`, `<T>x`, `!` and `satisfies` change no runtime
3536
+ // value, so every "is this expression THAT reference / THAT object?" question asks it through this one function.
3537
+ // There were seventeen hand-rolled loops answering it with FIVE different wrapper sets (parens only; +`as`;
3538
+ // +`!`; +`satisfies`; +`<T>`), and every gap was a spelling the engine went blind on: `(k.m as F).call(k)`,
3539
+ // `Reflect.apply(f as F, …)` and `(<any>globalThis).process.env.HOME` were ABSENT (EXECUTED, lane fixture
3540
+ // `fx/r780`), `(f as F).bind(t)` and `run((f))` were opaque. Two loops are deliberately NOT routed here:
3541
+ // `conversionSourceTracked` stops at `as` because an `as` IS the conversion it is asking about (R958 reads the
3542
+ // assertion separately), and `rootsAtStdStream` is a SUPPRESSOR (it frees `process.stdout.write` of a fabricated
3543
+ // Net) — widening a suppressor removes charges, which is not this fix's direction.
3544
+ // (Also R947: the invoked function's own ARGUMENTS on the reflective routes, or `null` when they are not
3545
+ // visible here — a spread, an `.apply` of a non-literal array. `null` is the safe answer: it captures nothing.)
3521
3546
  function unwrapArgExpr(e) {
3522
3547
  while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression?.(e)
3523
3548
  || ts.isNonNullExpression(e) || ts.isSatisfiesExpression?.(e))) e = e.expression;
@@ -3701,6 +3726,14 @@ function kappaOfRef(d2, ref) {
3701
3726
  function netEstablishingVerb(member, mod) {
3702
3727
  return NET_ESTABLISHING.has(member) || (/^(node:)?dgram$/.test(mod ?? "") && member === "send");
3703
3728
  }
3729
+ // ⟨R966⟩ see the (CLASSIFY) arm. A package whose `listen` is not a network accept: JSON-RPC connections start
3730
+ // reading a transport chosen (and, when it is a socket, charged) where the transport was built.
3731
+ const NON_NETWORK_LISTEN_PKGS = /^(vscode-jsonrpc|vscode-languageserver(-protocol)?|vscode-languageclient)(\/|$)/;
3732
+ function dependencyListenIsAccept(mod, decl) {
3733
+ if (!mod || mod === "<local>" || mod === "<es-lib>" || mod.startsWith("/") || declIsNodeTypes(decl) || isOwnPackageDecl(decl))
3734
+ return false;
3735
+ return !NON_NETWORK_LISTEN_PKGS.test(mod);
3736
+ }
3704
3737
  function netAcceptingVerb(member, kMod) {
3705
3738
  return NET_ACCEPTING.has(member) || (/^(node:)?inspector(\/promises)?$/.test(kMod ?? "") && member === "open");
3706
3739
  }
@@ -4142,6 +4175,14 @@ const CHA_FANOUT_LIMIT = 12;
4142
4175
  // rather than inlined for the reason `CHA_FANOUT_LIMIT` was: two literals for one rule is how they drift.
4143
4176
  const TYPINGS_CENSUS_CAP = 128;
4144
4177
  const classOverrides = new Map();// base-method MemberDeclaration node -> overriding subclass member nodes (class-CHA)
4178
+ // ⟨SOUNDNESS R873⟩ An override that is not a class MEMBER: `this.m = () => …` assigned inside an instance body of
4179
+ // class `C` replaces `m` on every `C` instance. Registered in `classOverrides` like a member, keyed to the class
4180
+ // that performs the assignment here, because the node's `.parent` is an expression, not the class.
4181
+ const assignedOverrideClass = new Map(); // assigned override node -> the class-like node whose instances it overrides
4182
+ // The class an override node belongs to, for receiver-subtree scoping: a member's own class (declaration OR
4183
+ // expression — ⟨R956⟩), or the assigning class of a ⟨R873⟩ assigned override.
4184
+ const overrideOwner = (om) => assignedOverrideClass.get(om)
4185
+ ?? ((om?.parent && (ts.isClassDeclaration(om.parent) || ts.isClassExpression(om.parent))) ? om.parent : null);
4145
4186
  const classDescendants = new Map();// base ClassDeclaration -> transitive LOCAL subclass ClassDeclarations (coercion-CHA)
4146
4187
  // R954 — the conformer registries (filled by the conversion pass after `walkStructural`).
4147
4188
  const classConformers = new Map(); // local ClassDeclaration -> conformer nodes (local class, literal, dep class)
@@ -4226,7 +4267,7 @@ function overrideClosure(seeds) {
4226
4267
  const out = [];
4227
4268
  for (const s of seeds) {
4228
4269
  for (const m of [s, ...overrideDescent(s)]) {
4229
- if (isOverridableMethod(m) && !out.includes(m)) out.push(m);
4270
+ if ((isOverridableMethod(m) || assignedOverrideClass.has(m)) && !out.includes(m)) out.push(m);
4230
4271
  if (out.length > CHA_FANOUT_LIMIT + 1) return out;
4231
4272
  }
4232
4273
  }
@@ -4277,7 +4318,7 @@ function overrideDescent(decl) {
4277
4318
  function memberDispatchBodies(decl, rootClass) {
4278
4319
  const all = overrideDescent(decl);
4279
4320
  const scoped = (!rootClass || all.length === 0) ? all
4280
- : all.filter((om) => ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass));
4321
+ : all.filter((om) => classInSubtree(overrideOwner(om), rootClass)); // R873/R956: assigned and class-expression overrides
4281
4322
  // R954 — …and the CONFORMERS registered at the receiver's class (a value the checker showed
4282
4323
  // converted to it, or to a subclass of it), which run for this dispatch exactly as an override does. Scoped
4283
4324
  // the same way: registered at `Ct` and its ancestors, so a SUBTYPE receiver never sees them.
@@ -5153,7 +5194,9 @@ for (const { mod, name, ident } of exportAliasCandidates) {
5153
5194
  const baseClassOf = localBaseClassOf;
5154
5195
  for (const sf of sources) {
5155
5196
  (function scan(node) {
5156
- if (ts.isClassDeclaration(node)) {
5197
+ // ⟨SOUNDNESS R956⟩ a CLASS EXPRESSION is a subclass like any other: `new (class extends BaseC { m(){…} })()`
5198
+ // reaching a `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED) because this index read declarations only.
5199
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
5157
5200
  // Local-DESCENDANT index (the coercion-CHA universe, below). classOverrides is keyed by an
5158
5201
  // ANCESTOR MEMBER, so it only sees an override whose base ALSO declares the name. The coercion
5159
5202
  // protocol's members are the opposite shape: `class Sub extends Base { toString(){…} }` where
@@ -5190,6 +5233,7 @@ for (const { mod, name, ident } of exportAliasCandidates) {
5190
5233
  if (!classOverrides.has(ancestor)) classOverrides.set(ancestor, []);
5191
5234
  classOverrides.get(ancestor).push(m);
5192
5235
  foundLocal = true;
5236
+ mintClassExprOverride(node, m, name); // R956
5193
5237
  break;
5194
5238
  }
5195
5239
  base = baseClassOf(base);
@@ -5209,16 +5253,79 @@ for (const { mod, name, ident } of exportAliasCandidates) {
5209
5253
  if (!foreignClassOverrides.has(fm)) foreignClassOverrides.set(fm, []);
5210
5254
  const arr = foreignClassOverrides.get(fm);
5211
5255
  if (!arr.includes(m)) arr.push(m);
5256
+ mintClassExprOverride(node, m, name); // R956
5212
5257
  }
5213
5258
  }
5214
5259
  }
5215
5260
  }
5216
5261
  }
5262
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) registerAssignedOverrides(node); // R873
5217
5263
  ts.forEachChild(node, scan);
5218
5264
  })(sf);
5219
5265
  }
5220
5266
  }
5221
5267
 
5268
+ // ⟨SOUNDNESS R956⟩ A class EXPRESSION's member has no unit (`localName` names class DECLARATION members only), so
5269
+ // its body folded into whatever evaluates the expression and no dispatch could land on it. An override needs an
5270
+ // addressable body: mint it the way a structural implementor's member is minted — keyed by `stableUnitTag`, with
5271
+ // ⟨R519⟩'s containment edge so the evaluating unit keeps every charge it had (an ADDITION, never a move). Only a
5272
+ // member that OVERRIDES is minted; a class expression's other members stay where they were.
5273
+ function mintClassExprOverride(cls, m, name) {
5274
+ if (!ts.isClassExpression(cls) || nodeName.has(m)) return;
5275
+ const bodied = (ts.isMethodDeclaration(m) && m.body)
5276
+ || (ts.isPropertyDeclaration(m) && m.initializer && (ts.isArrowFunction(m.initializer) || ts.isFunctionExpression(m.initializer)));
5277
+ if (!bodied) return; // an unminted override reads unresolved at every reader — disclosed, never silent
5278
+ const sf = m.getSourceFile();
5279
+ mintPositionalStructuralUnit(moduleOf(sf), sf, m, name);
5280
+ if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH classexpr ${name}`);
5281
+ }
5282
+ // ⟨SOUNDNESS R873⟩ `this.m = <value>` inside an INSTANCE body of class `C` (its constructor, a method, an accessor,
5283
+ // an arrow property — not a nested `function`, which rebinds `this`) replaces `m` on that instance. Where `C` or a
5284
+ // local ancestor declares METHOD `m`, the assignment is an override of it: a `Base`-typed `b.m()` on a `C` runs the
5285
+ // assigned value. MEASURED: `class SubC extends BaseC { constructor(){ super(); this.m = () => write } }` with a
5286
+ // `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED), the arrow charged to `SubC.constructor` only.
5287
+ // · a function literal is minted (`mintCallTargetUnit`, containment edge kept: the assigning body still carries it)
5288
+ // and registered — a resolution;
5289
+ // · `this.m = this.m.bind(…)` / `this.m = this.m` re-installs the same method and registers nothing;
5290
+ // · any other value (a wrapper's result, another method's `.bind`, a parameter) is registered UNRESOLVED, so every
5291
+ // reader's `allResolved` gate discloses `dispatch:` rather than certifying the declared body alone.
5292
+ // A PROPERTY `m` is not this rule's: a written property slot is ⟨R103⟩'s, which already discloses.
5293
+ function registerAssignedOverrides(cls) {
5294
+ const memberNamed = (c, name) => (c.members ?? []).find((x) => ts.isMethodDeclaration(x) && x.name?.getText?.() === name);
5295
+ for (const mem of cls.members ?? []) {
5296
+ if (ts.getCombinedModifierFlags(mem) & ts.ModifierFlags.Static) continue;
5297
+ const body = (ts.isConstructorDeclaration(mem) || ts.isMethodDeclaration(mem) || ts.isGetAccessorDeclaration(mem)
5298
+ || ts.isSetAccessorDeclaration(mem)) ? mem.body
5299
+ : (ts.isPropertyDeclaration(mem) && mem.initializer && ts.isArrowFunction(mem.initializer)) ? mem.initializer.body : null;
5300
+ if (!body) continue;
5301
+ (function walk(n) {
5302
+ if (n !== body && (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isClassLike(n))) return;
5303
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken
5304
+ && ts.isPropertyAccessExpression(n.left) && n.left.expression.kind === ts.SyntaxKind.ThisKeyword) {
5305
+ const name = n.left.name.getText();
5306
+ let anc = memberNamed(cls, name);
5307
+ for (let b = localBaseClassOf(cls), g = 0; !anc && b && g++ < 64; b = localBaseClassOf(b)) anc = memberNamed(b, name);
5308
+ if (anc) {
5309
+ const rhs = unwrapArgExpr(n.right);
5310
+ const bound = rhs && ts.isCallExpression(rhs) ? unwrapBind(rhs) : null;
5311
+ const ref = bound ? bound.ref : rhs;
5312
+ const same = ref && ts.isPropertyAccessExpression(ref) && ref.expression.kind === ts.SyntaxKind.ThisKeyword
5313
+ && ref.name.getText() === name;
5314
+ if (!same) {
5315
+ const node = rhs && (ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) ? rhs : n;
5316
+ if (node === rhs && !callTargetUnit(rhs)) mintCallTargetUnit(rhs);
5317
+ if (!classOverrides.has(anc)) classOverrides.set(anc, []);
5318
+ const arr = classOverrides.get(anc);
5319
+ if (!arr.includes(node)) { arr.push(node); assignedOverrideClass.set(node, cls); }
5320
+ if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH assigned ${node === rhs ? "fn" : "opaque"} ${name}`);
5321
+ }
5322
+ }
5323
+ }
5324
+ ts.forEachChild(n, walk);
5325
+ })(body);
5326
+ }
5327
+ }
5328
+
5222
5329
  // callback-flow bookkeeping (the Rust engine's callback_named move, ported): for every call that
5223
5330
  // edges to a LOCAL unit, record what each argument position received — a NAMED local unit (a
5224
5331
  // resolvable callback target), or an opaque value (an inline closure stays attributed to the
@@ -5233,7 +5340,8 @@ const callbackArgs = new Map(); // calleeName -> Map(argIndex -> Array<{calle
5233
5340
  // What each argument position of a call to LOCAL unit `targetName` received (callback flow, see `callbackArgs`).
5234
5341
  // Shared by the ordinary local arm and R955's callee-choice arm, so a callback passed through either is seen.
5235
5342
  function registerCallbackArgs(rec, targetName, args) {
5236
- args.forEach((a, i) => {
5343
+ args.forEach((a0, i) => {
5344
+ const a = unwrapArgExpr(a0); // R780: `run((f))` / `run(f as F)` pass the same function as `run(f)`
5237
5345
  const slot = (callbackArgs.get(targetName) ?? callbackArgs.set(targetName, new Map()).get(targetName));
5238
5346
  const list = slot.get(i) ?? [];
5239
5347
  let target = null, opaque = false;
@@ -5372,8 +5480,7 @@ if (R103_HITS) process.on("exit", () => {
5372
5480
  });
5373
5481
  function openCallSlot(node) {
5374
5482
  if (!ts.isCallExpression(node)) return null; // `new X()` constructs a class, not a slot
5375
- let callee = node.expression;
5376
- while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
5483
+ const callee = unwrapArgExpr(node.expression); // R780
5377
5484
  // ELEMENT ACCESS IS THE SAME SLOT BY ANOTHER SPELLING, and it is here because the first draft of this
5378
5485
  // comment asserted the opposite — "an element access already reaches the dynamic-key/`callback:` arms"
5379
5486
  // — and that was FALSE when measured: `this["handler"]()` and `this[k]()` (k narrowed to the literal
@@ -5439,8 +5546,8 @@ function unwrapBind(node, depth = 0) {
5439
5546
  if (!node || depth > 8) return null;
5440
5547
  if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
5441
5548
  if (node.expression.name.text !== "bind") return null;
5442
- let recv = node.expression.expression;
5443
- while (ts.isParenthesizedExpression(recv)) recv = recv.expression;
5549
+ // R780: `(f as F).bind(t)` / `(f satisfies F).bind(t)` bind the same function — the ONE wrapper set, not parens only.
5550
+ const recv = unwrapArgExpr(node.expression.expression);
5444
5551
  // chained `.bind().bind()` — recurse only when the receiver is ITSELF a `.bind` call, else it's an
5445
5552
  // arbitrary call (`getCallback().bind`) whose result we can't pin → unresolvable bind.
5446
5553
  if (ts.isCallExpression(recv)) {
@@ -6091,11 +6198,7 @@ function isConversionPosition(n) {
6091
6198
  }
6092
6199
  // R958 — the value under an assertion chain: parentheses, `as`, `<T>`, `!` and `satisfies` change no runtime value.
6093
6200
  let conversionViaAssertion = false; // set while `convert` records a source read THROUGH an assertion
6094
- function peelAssertions(e) {
6095
- while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression(e)
6096
- || ts.isNonNullExpression(e) || ts.isSatisfiesExpression?.(e))) e = e.expression;
6097
- return e;
6098
- }
6201
+ const peelAssertions = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a third copy of it
6099
6202
  // R82 / PART 87: is the conversion's SOURCE type a fact about the VALUE, or only about a generic signature? A
6100
6203
  // call whose declared return type mentions one of its own type parameters (`wrap<T>(x: T): T`) hands back
6101
6204
  // whatever type the ARGUMENT had, and that signature proves assignability, never that the value IS the
@@ -6129,11 +6232,175 @@ function conversionSourceTracked(e, depth = 0) {
6129
6232
  }
6130
6233
  return true;
6131
6234
  }
6235
+ // ---- PARAMETER PROVENANCE: what every VISIBLE call site binds to a parameter (SOUNDNESS R934, R246) ----
6236
+ // A parameter's value is whatever its callers pass. For a function whose every reference is a CALL — not
6237
+ // handed out as a value, not `.call`/`.apply`/`.bind`-ed — the visible call sites ARE its bindings, and the
6238
+ // arguments there say what the parameter can hold. Two consumers ask this ONE index rather than each
6239
+ // walking calls: the environment-location model (a parameter every site hands `process.env`, R934) and
6240
+ // the `any`-receiver accessor arm (an `any`-typed parameter whose every site passes a typed value, R246).
6241
+ // `paramArgs` maps a parameter key (its symbol; a destructured parameter's NODE) to the argument expression
6242
+ // at each visible site (`null` where a spread hides the position; the default, or `null` for `undefined`,
6243
+ // where the site omits it). `paramFnEscaped(fnSym)` answers whether the function escapes, batched.
6244
+ const paramArgs = new Map();
6245
+ const paramOwnerOf = new Map(); // param key -> function symbol (for the escape question)
6246
+ const paramDeclOf = new Map(); // param key -> its ParameterDeclaration
6247
+ let paramFnEscaped;
6248
+ // A class some project class `extends` — its members are excluded (an override is invisible here).
6249
+ const extendedClassSyms = new Set();
6250
+ const classSymOf = (cls) => (cls?.name ? checker.getSymbolAtLocation(cls.name) : cls?.symbol) ?? null;
6251
+ {
6252
+ const noteArg = (k, v) => { if (k) (paramArgs.get(k) ?? paramArgs.set(k, []).get(k)).push(v); };
6253
+ const collectHeritage = (node) => {
6254
+ if ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) && node.heritageClauses) {
6255
+ for (const h of node.heritageClauses) if (h.token === ts.SyntaxKind.ExtendsKeyword) {
6256
+ for (const t of h.types) {
6257
+ let s = checker.getSymbolAtLocation(t.expression);
6258
+ if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
6259
+ if (s) extendedClassSyms.add(s);
6260
+ }
6261
+ }
6262
+ }
6263
+ ts.forEachChild(node, collectHeritage);
6264
+ };
6265
+ for (const sf of sources) collectHeritage(sf);
6266
+ const calls = [];
6267
+ const collectCalls = (node) => {
6268
+ if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) > 0) calls.push(node);
6269
+ ts.forEachChild(node, collectCalls);
6270
+ };
6271
+ for (const sf of sources) collectCalls(sf);
6272
+ // ⟨R934⟩ the functions whose parameters are eligible locations: a function declaration, a function or
6273
+ // arrow bound to a `const`, a constructor, or a method of a class with no heritage that nothing extends.
6274
+ // Every reference to it must be a CALL (or an export, a `typeof`, a member read that is not
6275
+ // `.call`/`.apply`/`.bind`) — anything else hands it to code that binds its parameters unseen.
6276
+ const fnEligible = new Map(); // declaration -> boolean
6277
+ const fnSymOf = (decl) => {
6278
+ if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) return decl.name ? checker.getSymbolAtLocation(decl.name) : null;
6279
+ if ((ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) && ts.isVariableDeclaration(decl.parent)
6280
+ && decl.parent.initializer === decl && ts.isIdentifier(decl.parent.name)
6281
+ && (ts.getCombinedNodeFlags(decl.parent) & ts.NodeFlags.Const)) return checker.getSymbolAtLocation(decl.parent.name);
6282
+ if (ts.isConstructorDeclaration(decl)) return classSymOf(decl.parent);
6283
+ return null;
6284
+ };
6285
+ const declShapeOk = (decl) => {
6286
+ if (!decl?.body || !projectFiles.has(path.resolve(decl.getSourceFile().fileName))) return false;
6287
+ if (ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) return true;
6288
+ if (ts.isConstructorDeclaration(decl)) return true;
6289
+ if (ts.isMethodDeclaration(decl) && (ts.isClassDeclaration(decl.parent) || ts.isClassExpression(decl.parent)))
6290
+ return !decl.parent.heritageClauses?.length && !extendedClassSyms.has(classSymOf(decl.parent));
6291
+ return false;
6292
+ };
6293
+ const escapedFnSyms = new Set();
6294
+ const escapeScannedSyms = new Set();
6295
+ // One traversal per BATCH of function symbols not yet asked about; the answer is cached.
6296
+ const scanEscapes = (cands0) => {
6297
+ const cands = new Set([...cands0].filter((x) => x && !escapeScannedSyms.has(x)));
6298
+ if (!cands.size) return;
6299
+ for (const c of cands) escapeScannedSyms.add(c);
6300
+ const names = new Set([...cands].map((s) => s.name));
6301
+ const SAFE_MEMBER_BLOCK = new Set(["call", "apply", "bind"]);
6302
+ const visit = (node) => {
6303
+ if (ts.isIdentifier(node) && names.has(node.text)) {
6304
+ let s = checker.getSymbolAtLocation(node);
6305
+ if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
6306
+ if (s && cands.has(s) && !escapedFnSyms.has(s)) {
6307
+ // climb the transparent wrappers to the position the value lands in
6308
+ let cur = node;
6309
+ const p0 = node.parent;
6310
+ let safe = false;
6311
+ if (p0 && (ts.isPropertyAccessExpression(p0) || ts.isPropertyAssignment(p0) || ts.isMethodDeclaration(p0)
6312
+ || ts.isFunctionDeclaration(p0) || ts.isClassDeclaration(p0) || ts.isClassExpression(p0)
6313
+ || ts.isVariableDeclaration(p0) || ts.isPropertyDeclaration(p0)) && p0.name === node) {
6314
+ // a declaration name, or `o.name` — a METHOD reference: safe only as the callee of a call
6315
+ if (ts.isPropertyAccessExpression(p0)) {
6316
+ const pp = p0.parent;
6317
+ safe = !!pp && (ts.isCallExpression(pp) || ts.isNewExpression(pp)) && pp.expression === p0;
6318
+ } else safe = true;
6319
+ } else {
6320
+ while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent))) cur = cur.parent;
6321
+ const p = cur.parent;
6322
+ if (!p) safe = true;
6323
+ else if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === cur) safe = true;
6324
+ else if (ts.isPropertyAccessExpression(p) && p.expression === cur) safe = !SAFE_MEMBER_BLOCK.has(p.name.text);
6325
+ else if (ts.isExportSpecifier(p) || ts.isExportAssignment(p) || ts.isTypeQueryNode(p)
6326
+ || ts.isTypeOfExpression(p) || ts.isExpressionWithTypeArguments(p) || ts.isImportSpecifier(p)
6327
+ || ts.isImportClause(p) || ts.isNamespaceImport(p) || ts.isTypeReferenceNode(p)
6328
+ || ts.isQualifiedName(p)) safe = true;
6329
+ else if (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) safe = true;
6330
+ }
6331
+ if (!safe) escapedFnSyms.add(s);
6332
+ }
6333
+ }
6334
+ ts.forEachChild(node, visit);
6335
+ };
6336
+ for (const sf of sources) visit(sf);
6337
+ };
6338
+ // Seed the parameter bindings: for every call whose resolved declaration is an eligible shape, bind each
6339
+ // identifier parameter to its argument (or default / undefined). Only calls that hand over at least one
6340
+ // argument are recorded here — a site that passes nothing binds `undefined` to every parameter, which
6341
+ // is recorded by the per-declaration site count below.
6342
+ const sitesByDecl = new Map(); // decl -> number of call sites seen
6343
+ for (const call of calls) {
6344
+ let decl;
6345
+ try { decl = checker.getResolvedSignature(call)?.declaration; } catch { decl = null; }
6346
+ if (!decl || !declShapeOk(decl)) continue;
6347
+ if (!fnEligible.has(decl)) fnEligible.set(decl, !!fnSymOf(decl));
6348
+ if (!fnEligible.get(decl)) continue;
6349
+ sitesByDecl.set(decl, (sitesByDecl.get(decl) ?? 0) + 1);
6350
+ const fsym = fnSymOf(decl);
6351
+ let pos = 0, sawSpread = false;
6352
+ const args = call.arguments ?? [];
6353
+ const argAt = [];
6354
+ for (const a of args) { if (ts.isSpreadElement(a)) { sawSpread = true; break; } argAt[pos++] = a; }
6355
+ decl.parameters.forEach((p, i) => {
6356
+ if (p.dotDotDotToken) return;
6357
+ // a DESTRUCTURED parameter reads its keys when the frame is entered — keyed by the parameter node
6358
+ const ps = ts.isIdentifier(p.name) ? checker.getSymbolAtLocation(p.name)
6359
+ : ts.isObjectBindingPattern(p.name) ? p : null;
6360
+ if (!ps) return;
6361
+ paramOwnerOf.set(ps, fsym);
6362
+ if (i < argAt.length && argAt[i]) noteArg(ps, argAt[i]);
6363
+ else if (sawSpread) noteArg(ps, null);
6364
+ else noteArg(ps, p.initializer ?? null);
6365
+ });
6366
+ }
6367
+ // A call site that resolves to the declaration with NO arguments was not collected above; it binds
6368
+ // every parameter to its default or `undefined`. Count them so a zero-argument site is not lost.
6369
+ {
6370
+ const visitZero = (node) => {
6371
+ if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) === 0) {
6372
+ let decl;
6373
+ try { decl = checker.getResolvedSignature(node)?.declaration; } catch { decl = null; }
6374
+ if (decl && sitesByDecl.has(decl)) for (const p of decl.parameters)
6375
+ if (!p.dotDotDotToken && ts.isIdentifier(p.name)) noteArg(checker.getSymbolAtLocation(p.name), p.initializer ?? null);
6376
+ else if (!p.dotDotDotToken && ts.isObjectBindingPattern(p.name)) noteArg(p, p.initializer ?? null);
6377
+ }
6378
+ ts.forEachChild(node, visitZero);
6379
+ };
6380
+ if (sitesByDecl.size) for (const sf of sources) visitZero(sf);
6381
+ }
6382
+ for (const k of paramArgs.keys()) { const d = ts.isParameter(k) ? k : k.valueDeclaration; if (d && ts.isParameter(d)) paramDeclOf.set(k, d); }
6383
+ paramFnEscaped = (fsym) => { if (!fsym) return true; scanEscapes([fsym]); return escapedFnSyms.has(fsym); };
6384
+ // Batch form: answer for many at once with ONE traversal.
6385
+ paramFnEscaped.batch = (fsyms) => scanEscapes(fsyms);
6386
+ }
6387
+
6132
6388
  const CONFORMER_REACH = process.env.CANDOR_CONFORMER_REACH ? (k, n) => console.error(`CONFORMER-REACH ${k} ${n}`) : null;
6133
6389
  for (const sf of sources) {
6134
6390
  const convert = (src, tt, at, kind) => {
6135
6391
  if (!tt || !conversionSourceTracked(src)) return;
6136
6392
  let st; try { st = checker.getTypeAtLocation(src); } catch { st = undefined; }
6393
+ // ⟨SOUNDNESS R958, the `any`-PARAMETER half⟩ a source typed `any`/`unknown` names no conformer, so a value
6394
+ // laundered through an `any` parameter reached a closed dispatch silently: `launder(x: any) { qDisp(x) }`
6395
+ // called as `launder(new LocalW())` ran LocalW's `m` (EXECUTED) with `deny Fs` exit 0 on the caller. The
6396
+ // parameter's visible arguments say what it holds — the SAME provenance index R246's accessor arm asks
6397
+ // (`receiverValueTypes`) — so each argument type is recorded as a conversion source. Upcasts only, as
6398
+ // ever (`recordConversion` keeps a target the type is assignable to). A dependency-sourced value and a
6399
+ // lying downcast are untouched: they are R958's by-design residue.
6400
+ if (anyishType(st)) for (const vt of receiverValueTypes(src)) {
6401
+ try { recordConversion(vt, tt); } catch { /* best effort, as the main record below */ }
6402
+ CONFORMER_REACH?.("any-param", `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
6403
+ }
6137
6404
  conversionViaAssertion = kind !== "conversion";
6138
6405
  try {
6139
6406
  if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
@@ -6308,6 +6575,47 @@ function keyLiteralNames(t, depth = 0, seen = new Set()) {
6308
6575
  if (c && c !== t) return keyLiteralNames(c, depth + 1, seen);
6309
6576
  return null; // `string`, `symbol`, `keyof T` for generic T, …
6310
6577
  }
6578
+ // SOUNDNESS R246 — THE RECEIVER'S VALUE, WHEN ITS STATIC TYPE SAYS NOTHING. Accessor lookup asks the
6579
+ // receiver's TYPE which properties it declares, and `any` declares none: `(s as any)[k] = v`,
6580
+ // `(s as any).token = v` and `function w(o: any) { o.token = v }` called as `w(new S())` all invoked S's
6581
+ // setter (EXECUTED, 6 of 6 writes observed) and were ABSENT with `deny Fs <fn>` exit 0 — the caller too.
6582
+ // The value is not unknowable; only its static type was thrown away. Two places say what it really is,
6583
+ // and both are facts rather than guesses:
6584
+ // - an ASSERTION changes no runtime value (R958's look-through, the same `peelAssertions`): the operand
6585
+ // under `as any` / `as unknown as T` / `<any>x` has the type the value really carries;
6586
+ // - an `any`/`unknown` PARAMETER holds what its visible call sites pass (the shared PARAMETER PROVENANCE
6587
+ // index, R934) — only for a function whose every reference is a call; an escaped one is unanswered.
6588
+ // Returns the EXTRA types beyond the checker's own (empty when the checker's type is informative and no
6589
+ // assertion intervenes), so a typed receiver's lookup is byte-identical to before. This RESOLVES — it adds
6590
+ // the accessor edges the value's type declares — and never hedges; a receiver neither rule answers is
6591
+ // exactly as it was (the residual is stated in SOUNDNESS R246, not hidden here).
6592
+ function anyishType(t) { return !!t && !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)); } // hoisted: the conversion walk asks it first
6593
+ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
6594
+ if (!expr || depth > 6) return [];
6595
+ let t; try { t = checker.getTypeAtLocation(expr); } catch { t = null; }
6596
+ const inner = peelAssertions(expr);
6597
+ if (inner && inner !== expr) {
6598
+ const below = receiverValueTypes(inner, depth + 1, seen);
6599
+ let ti; try { ti = checker.getTypeAtLocation(inner); } catch { ti = null; }
6600
+ return [...(ti && !anyishType(ti) ? [ti] : []), ...below];
6601
+ }
6602
+ if (!anyishType(t) || !ts.isIdentifier(expr)) return [];
6603
+ const sym = checker.getSymbolAtLocation(expr);
6604
+ const d = sym?.valueDeclaration;
6605
+ if (!sym || !d || !ts.isParameter(d) || seen.has(sym)) return [];
6606
+ seen.add(sym);
6607
+ const args = paramArgs.get(sym);
6608
+ if (!args || paramFnEscaped(paramOwnerOf.get(sym))) return [];
6609
+ const out = [];
6610
+ for (const a of args) {
6611
+ if (!a) continue;
6612
+ let ta; try { ta = checker.getTypeAtLocation(a); } catch { ta = null; }
6613
+ if (ta && !anyishType(ta)) out.push(ta);
6614
+ out.push(...receiverValueTypes(a, depth + 1, seen));
6615
+ }
6616
+ return out;
6617
+ }
6618
+ const dedupeHits = (hits) => { const seen = new Set(); return hits.filter((h) => !seen.has(h.decl) && seen.add(h.decl)); };
6311
6619
  function accessorsAt(propNode, kind /* "get" | "set" */) {
6312
6620
  if (ts.isElementAccessExpression(propNode)) {
6313
6621
  // `c["prop"]` carries no `.name`; resolve the key to the property NAMES it can hold and look each
@@ -6324,9 +6632,19 @@ function accessorsAt(propNode, kind /* "get" | "set" */) {
6324
6632
  // property is common to all of them).
6325
6633
  for (const t of texts) for (const sym of propertySymbolsAcrossArms(recvType, t))
6326
6634
  out.push(...accessorsFromSym(sym, kind));
6327
- return out;
6328
- }
6329
- return accessorsFromSym(checker.getSymbolAtLocation(propNode.name ?? propNode), kind);
6635
+ // R246 — and every type the receiver's VALUE can carry under an assertion or an `any` parameter.
6636
+ for (const vt of receiverValueTypes(propNode.expression))
6637
+ for (const t of texts) for (const sym of propertySymbolsAcrossArms(vt, t)) out.push(...accessorsFromSym(sym, kind));
6638
+ return dedupeHits(out);
6639
+ }
6640
+ const direct = accessorsFromSym(checker.getSymbolAtLocation(propNode.name ?? propNode), kind);
6641
+ if (direct.length || !ts.isPropertyAccessExpression(propNode)) return direct;
6642
+ // R246 — `(s as any).token = v` / `o.token = v` with `o: any`: the checker resolves no symbol at all.
6643
+ const out = [];
6644
+ const name = propNode.name.text;
6645
+ for (const vt of receiverValueTypes(propNode.expression))
6646
+ for (const sym of propertySymbolsAcrossArms(vt, name)) out.push(...accessorsFromSym(sym, kind));
6647
+ return dedupeHits(out);
6330
6648
  }
6331
6649
  // A `Object.defineProperty` descriptor accessor for the forcing site `recv.key` (read → get, assign →
6332
6650
  // set), consulted ONLY when the type-level `accessorsAt` came up empty (the checker types target.key as
@@ -6406,8 +6724,7 @@ function accessorOverrideFanOut(rec, decl, recvExpr) {
6406
6724
  // SOUNDNESS-PRESERVING FALLBACK, the method path's verbatim: a receiver we cannot pin to a LOCAL
6407
6725
  // class (a union, an interface, `any`, an external type) keeps the FULL override set.
6408
6726
  const rootClass = localReceiverClass(recvExpr);
6409
- const direct = rootClass ? allOverrides.filter((om) =>
6410
- ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
6727
+ const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
6411
6728
  const overrides = memberDispatchBodies(decl, rootClass);
6412
6729
  if (direct.length <= CHA_FANOUT_LIMIT) probeR871("accessor", decl, overrides.length - direct.length);
6413
6730
  if (overrides.length === 0) return;
@@ -6658,8 +6975,10 @@ function classBodiedGetter(sym) {
6658
6975
  }
6659
6976
  function enumerateGetters(owner, type, srcExpr) {
6660
6977
  if (!owner) return;
6661
- if (type && type.getProperties) {
6662
- for (const p of propertiesAcrossArms(type)) { // R259 — the disjunction, not the intersection
6978
+ // R246 — the copied VALUE's types too, when its static type is `any`/`unknown` or asserted away.
6979
+ const vprops = srcExpr ? receiverValueTypes(srcExpr).flatMap((vt) => propertiesAcrossArms(vt)) : [];
6980
+ if ((type && type.getProperties) || vprops.length) {
6981
+ for (const p of [...(type?.getProperties ? propertiesAcrossArms(type) : []), ...vprops]) { // R259 — the disjunction, not the intersection
6663
6982
  if (classBodiedGetter(p)) continue; // prototype + non-enumerable → not copied by a spread
6664
6983
  for (const hit of accessorsFromSym(p, "get")) recordAccessorHit(owner, hit, p.getName(), srcExpr);
6665
6984
  }
@@ -6670,9 +6989,7 @@ function enumerateGetters(owner, type, srcExpr) {
6670
6989
  //
6671
6990
  // R120 — THREE UNWRAPS AND ONE ALIAS HOP, because the question is which OBJECT is being copied and
6672
6991
  // none of these four wrappers changes that answer. Each is a spelling measured absent at e5c60bc.
6673
- let se = srcExpr;
6674
- while (se && (ts.isParenthesizedExpression(se) || ts.isAsExpression(se)
6675
- || ts.isSatisfiesExpression(se) || ts.isNonNullExpression(se))) se = se.expression;
6992
+ const se = unwrapArgExpr(srcExpr); // R780
6676
6993
  if (!se || !ts.isIdentifier(se)) return; // parameter / call return / member
6677
6994
  let sym0 = checker.getSymbolAtLocation(se);
6678
6995
  if (!sym0) return;
@@ -6682,9 +6999,7 @@ function enumerateGetters(owner, type, srcExpr) {
6682
6999
  if (sym0.flags & ts.SymbolFlags.Alias) { try { sym0 = checker.getAliasedSymbol(sym0) ?? sym0; } catch { /* unresolved import */ } }
6683
7000
  for (const d of sym0.declarations ?? []) {
6684
7001
  if (!ts.isVariableDeclaration(d) || !d.initializer) continue;
6685
- let init = d.initializer;
6686
- while (ts.isParenthesizedExpression(init) || ts.isAsExpression(init)
6687
- || ts.isSatisfiesExpression(init) || ts.isNonNullExpression(init)) init = init.expression;
7002
+ const init = unwrapArgExpr(d.initializer); // R780
6688
7003
  if (!ts.isObjectLiteralExpression(init)) continue;
6689
7004
  for (const pr of init.properties) {
6690
7005
  if (!ts.isGetAccessorDeclaration(pr)) continue;
@@ -6729,8 +7044,7 @@ function enumerateGetters(owner, type, srcExpr) {
6729
7044
  const provenCopiedKeys = (sources) => {
6730
7045
  const keys = new Set();
6731
7046
  for (let s of sources) {
6732
- while (s && (ts.isParenthesizedExpression(s) || ts.isAsExpression(s)
6733
- || ts.isSatisfiesExpression(s) || ts.isNonNullExpression(s))) s = s.expression;
7047
+ s = unwrapArgExpr(s); // R780
6734
7048
  if (!s || !ts.isObjectLiteralExpression(s)) return null; // parameter / call return / variable
6735
7049
  for (const pr of s.properties) {
6736
7050
  if (ts.isSpreadAssignment(pr)) return null; // `{...o}` copies an unknown key set
@@ -6808,12 +7122,15 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
6808
7122
  if (!owner || !targetExpr) return;
6809
7123
  const t = checker.getTypeAtLocation(targetExpr);
6810
7124
  if (!t || !t.getProperties) return;
7125
+ // R246 — every property the target's VALUE can carry: its static type's, and (under an assertion or an
7126
+ // `any`/`unknown` parameter) the types the value really has. One list for both branches below.
7127
+ const tprops = [...propertiesAcrossArms(t), ...receiverValueTypes(targetExpr).flatMap((vt) => propertiesAcrossArms(vt))];
6811
7128
  // `!keys`, not `keys === null`: a Set is always truthy, so this is exactly "the key set is not
6812
7129
  // provable" — and it keeps the tolerance the old `keys && !keys.has(…)` line had for a caller that
6813
7130
  // passes nothing. `keys.has` below is now unguarded, so a future third call site omitting the
6814
7131
  // argument would throw rather than take this branch.
6815
7132
  if (!keys) {
6816
- for (const p of propertiesAcrossArms(t)) { // R259 — the disjunction, not the intersection
7133
+ for (const p of tprops) { // R259 — the disjunction, not the intersection
6817
7134
  if (!accessorsFromSym(p, kind).length) continue;
6818
7135
  if (!keyCouldNameAccessor(p, unprovable.keyType ?? null)) continue;
6819
7136
  const rec = fns.get(owner);
@@ -6823,7 +7140,9 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
6823
7140
  }
6824
7141
  return; // a target declaring no reachable accessor discloses nothing (0 real invocations)
6825
7142
  }
6826
- for (const p of propertiesAcrossArms(t)) { // R259 — the disjunction, not the intersection
7143
+ const seenP = new Set();
7144
+ for (const p of tprops) { // R259 — the disjunction, not the intersection
7145
+ if (seenP.has(p)) continue; seenP.add(p);
6827
7146
  // NO `classBodiedGetter`-style exclusion here, and the asymmetry is the point rather than an
6828
7147
  // oversight: R115 excluded a class-bodied GETTER because a prototype accessor is non-enumerable and
6829
7148
  // therefore never COPIED. A prototype SETTER is the opposite — it is found by the assignment's
@@ -6927,6 +7246,29 @@ function decoratorArgUnit(callNode) {
6927
7246
  }
6928
7247
  return qual;
6929
7248
  }
7249
+ // ⟨SOUNDNESS R815⟩ The `extends <expr>` unit of a class that has a constructor unit. The expression runs ONCE, at
7250
+ // class definition (so the evaluating unit edges here, pass 2a′) — and every construction then runs the
7251
+ // constructor of the class it produced through `super`, implicit or explicit, whose body this scan sees only as
7252
+ // the expression's own callees (`mixin()`'s class expression is attributed to `mixin`; a dependency's
7253
+ // `PartialType(X)` is the dependency's ledger/`invisible`). So the constructor edges here too. Attributing the
7254
+ // expression to the constructor ALONE (the old climb) left the definer silent; attributing it to the definer
7255
+ // ALONE drops the super reach — MEASURED: `new D3()` over `class D3 extends Partial(Plain)` from a dependency
7256
+ // that writes in its generated constructor (EXECUTED) went from `invisible:['mixdep']` to ABSENT, and nest's six
7257
+ // `extends PartialType(…)` DTO constructors lost their `@nestjs/mapped-types` disclosure the same way.
7258
+ function heritageUnit(cls, ctorQual) {
7259
+ const qual = ctorQual.replace(/\.constructor$/, ".<heritage>");
7260
+ if (!fns.has(qual)) {
7261
+ const sf = cls.getSourceFile();
7262
+ const h = (cls.heritageClauses ?? []).find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) ?? cls;
7263
+ fns.set(qual, { local: `${fns.get(ctorQual)?.local?.replace(/\.constructor$/, "") ?? "<anonymous>"}.<heritage>`,
7264
+ direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
7265
+ cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(), why: new Set(),
7266
+ entry: false, unitKind: "initializer",
7267
+ loc: `${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(h.getStart()).line + 1}:1`,
7268
+ endLine: sf.getLineAndCharacterOfPosition(h.getEnd()).line + 1 });
7269
+ }
7270
+ return qual;
7271
+ }
6930
7272
  // nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
6931
7273
  function enclosing(node) {
6932
7274
  let prev = null;
@@ -6966,6 +7308,36 @@ function enclosing(node) {
6966
7308
  if (ts.isDecorator(p)) return null;
6967
7309
  const n = nodeName.get(p);
6968
7310
  if (n) return n;
7311
+ // ⟨SOUNDNESS R815⟩ THE REST OF THE CLASS-DEFINITION-TIME FAMILY R782/R785 started. Three more pieces of a class
7312
+ // run when the class is EVALUATED, not when it is constructed or a member is called — and the climb
7313
+ // attributed each to a unit that never runs it, leaving the evaluating unit silent (EXECUTED, lane fixture
7314
+ // `fx/r815`: the enclosing function ABSENT, `deny Fs` exit 0; the effect fabricated onto `C.constructor` or the
7315
+ // method):
7316
+ // · a `static x = …` INITIALISER runs at definition, exactly like a `static {}` block, so it lands on the
7317
+ // same `<static-init>` unit (which pass 2a′ wires from the evaluating unit). Climbing on reached the
7318
+ // ClassDeclaration, i.e. `C.constructor` — every `new C()` charged, the definer silent. An instance field
7319
+ // is NOT this (it runs per construction; the constructor is right), and a property that IS a unit
7320
+ // (`static h = () => …` minted as `C.h`) answered above before reaching here.
7321
+ // · an `extends <expr>` HERITAGE expression and
7322
+ // · a COMPUTED member name `[k()]` (class OR object-literal member)
7323
+ // are evaluated in the scope AROUND the class / literal, by whatever evaluates it — so the climb resumes
7324
+ // OUTSIDE the container. (The computed name hung under the member's own node, so the method `C3.[w("k")]`
7325
+ // was charged for its own name.)
7326
+ if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
7327
+ && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
7328
+ && p.parent && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent)))
7329
+ { R815_REACH?.("static"); return staticBlockUnit(p); }
7330
+ if (ts.isHeritageClause(p) && p.token === ts.SyntaxKind.ExtendsKeyword && p.parent
7331
+ && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent))) {
7332
+ const ctor = nodeName.get(p.parent);
7333
+ if (ctor) { R815_REACH?.("heritage"); return heritageUnit(p.parent, ctor); }
7334
+ // a class with no constructor unit: the climb already resumes outside it, at the evaluating unit
7335
+ }
7336
+ if (ts.isComputedPropertyName(p) && p.parent?.parent) {
7337
+ const container = p.parent.parent;
7338
+ if (ts.isClassDeclaration(container) || ts.isClassExpression(container)) { R815_REACH?.("ckey"); return enclosing(container.parent); }
7339
+ if (ts.isObjectLiteralExpression(container)) { R815_REACH?.("okey"); return enclosing(container); }
7340
+ }
6969
7341
  // Reached the SourceFile with no named unit: a TOP-LEVEL executable statement. Attribute to the
6970
7342
  // file's synthesized `<module>` initializer unit (minted lazily here) rather than dropping it.
6971
7343
  if (ts.isSourceFile(p)) return moduleUnit(p);
@@ -7204,8 +7576,7 @@ function recordDispatch(rec, decl, pkg) {
7204
7576
  function packageProducedReceiver(recvExpr, pkg) {
7205
7577
  if (!recvExpr || !pkg) return null;
7206
7578
  const unwrap = (x) => {
7207
- while (x && (ts.isAwaitExpression(x) || ts.isParenthesizedExpression(x) || ts.isNonNullExpression(x)
7208
- || ts.isAsExpression(x))) x = x.expression;
7579
+ for (x = unwrapArgExpr(x); x && ts.isAwaitExpression(x);) x = unwrapArgExpr(x.expression); // R780: + `await`
7209
7580
  return x;
7210
7581
  };
7211
7582
  let e = unwrap(recvExpr);
@@ -8357,6 +8728,89 @@ const HOF_INVOKERS = new Set([
8357
8728
  "then", "catch", "finally", "nextTick",
8358
8729
  ]);
8359
8730
 
8731
+ // ⟨SOUNDNESS R803⟩ THE LIST ABOVE IS NO LONGER THE GATE — IT IS THE POSITION MAP FOR THE CALLEES IT NAMES.
8732
+ //
8733
+ // `HOF_INVOKERS` decided, by callee NAME, whether a function reference handed to a non-local callee could be
8734
+ // invoked by it, and every callee it did not name dropped the reference: no edge, no `Unknown`, no row.
8735
+ // Measured on `fx/r803`, every cell EXECUTED by node to run its handler: `e.on`, `e.once`, `e.addListener`,
8736
+ // `process.once`, `http.createServer(h)`, `Array.from(xs, f)`, `new Promise(exec)`, lodash `_.each`/`_.times`,
8737
+ // a dependency `queue.add(f)` and `EventTarget.addEventListener` were all ABSENT, `deny Fs` exit 0 on each
8738
+ // caller. The comment that guarded it called the omission "an honest under-report (sound)" — true about
8739
+ // fabrication, false about silence; it also falsified `KAPPA_PURE`'s premise that lodash/rxjs side effects
8740
+ // "live in visible user callbacks", because this list is what decides whether those callbacks are visible.
8741
+ //
8742
+ // WHY NOT "ASK THE SIGNATURE" ALONE — measured by the ts vein analysis on the 28-entry roster: of 253 sites
8743
+ // where a callee's parameter is callable-typed and the argument is a local function reference, about half are
8744
+ // REMOVALS or STORES (`off`, `removeListener`, `removeEventListener`, `Map.set`, `Object.assign`), and 15 of the
8745
+ // 25 effect-bearing ones never invoke. A signature rule alone fabricates on those.
8746
+ //
8747
+ // So the rule is: an UNLISTED non-local callee invokes every argument position its resolved signature
8748
+ // POSITIVELY declares callable (`calleeParamIsCallable === true`), unless the callee is on the DENYLIST below.
8749
+ // The failure directions, stated because they are the reason for the shape (`candor-denylist-over-allowlist`):
8750
+ // · a non-invoker MISSING from the denylist → an over-charge (its reference's effects charged to the
8751
+ // caller). Visible, and the corpus A/B is the instrument that catches it.
8752
+ // · an invoker the old list missed → now charged. Nothing an invoker can do lands it back in silence
8753
+ // except being DENYLISTED, and the denylist only names declarations whose semantics are a removal or a
8754
+ // store — the removal NAMES on any receiver, and reviewed ES-lib / node-core members by owner.
8755
+ // · a callee whose parameter is `any`/`unknown`/unresolvable (`null`) is NOT admitted here. That is a
8756
+ // residual, named rather than hidden: `console.log(f)`/`assert.equal(f, …)` take `any` and never invoke,
8757
+ // and admitting `null` charges them — priced on the roster before deciding (see the commit message).
8758
+ // The listed names keep their own position map (`hofInvokesArg`/`hofArgIsNeverCallback`) unchanged.
8759
+ //
8760
+ // Removal by NAME on ANY receiver: a method named `removeListener` that invokes the listener it is removing
8761
+ // would be perverse, and these four are the registration API's inverse everywhere they appear (node events,
8762
+ // DOM `EventTarget`, eventemitter3, socket.io's emitter). Asked of the callee's spelling AND of the declaration
8763
+ // it resolved to, because `const processOff = process.off.bind(process); processOff(sig, h)` (vitest) and
8764
+ // got's `removeSessionSocketListener` are the same removal under a local name.
8765
+ //
8766
+ // Everything else is asked of the RESOLVED REVIEWED-LIBRARY declaration (ES lib, @types/node), never of a
8767
+ // name, so a DEPENDENCY's `add(fn)` (p-queue — a real roster silence) is not excused by sharing a name with
8768
+ // `Set.add`. The rule there is the GENERIC VALUE SLOT: a parameter whose declared type is a bare type
8769
+ // parameter (alone or beside non-callable members such as `PromiseLike<T>`) whose constraint has no call
8770
+ // signature. Such a callee is handed a value it knows nothing about and cannot call without a cast — `Map.set`'s
8771
+ // `V`, `Set.add`/`includes`/`indexOf`'s `T`, `Object.assign`/`defineProperties`/`freeze`'s `T`, a promise
8772
+ // executor's `resolve(value: T | PromiseLike<T>)`, `new Proxy(target: T)`, `new WeakRef(target: T)`. The
8773
+ // argument reads callable only because the CALL instantiated `T` with a function type. Restricted to the
8774
+ // reviewed library declarations on purpose: a dependency CAN cast and call (`(x as any)()`), so there the
8775
+ // generic slot is an assumption, and an assumption is not denylist evidence.
8776
+ // ⟨R241⟩ lib.dom interfaces whose every member touches a persistent browser store (see the es-lib call arm).
8777
+ const BROWSER_STORE_IFACES = new Set(["IDBFactory", "IDBDatabase", "IDBObjectStore", "IDBIndex", "IDBCursor",
8778
+ "IDBCursorWithValue", "IDBTransaction", "CacheStorage", "Cache", "StorageManager", "FileSystemHandle",
8779
+ "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemWritableFileStream", "FileSystemSyncAccessHandle", "CookieStore"]);
8780
+ const HOF_REMOVAL_NAMES = new Set(["off", "removeListener", "removeEventListener", "removeAllListeners"]);
8781
+ // NOT non-invokers — they invoke their first argument — but the reflective arm below answers them with the
8782
+ // invoked function's OWN arguments (R947's locator), and this arm, which cannot see those, would add a second
8783
+ // locator-less charge beside it (measured: `Reflect.apply(fs.readFileSync, undefined, ["/tmp/ok/b"])` lost its
8784
+ // `allow Fs /tmp/ok` certification). Owned elsewhere, so excluded here.
8785
+ const HOF_OWNED_ELSEWHERE = new Set(["Reflect.apply", "Reflect.construct"]);
8786
+ const hofOwnerMember = (decl) => {
8787
+ if (!decl) return null;
8788
+ const member = ts.isConstructSignatureDeclaration(decl) ? "new" : decl.name?.getText?.();
8789
+ const holder = decl.parent && ts.isModuleBlock(decl.parent) ? decl.parent.parent : decl.parent; // `namespace Reflect {…}`
8790
+ const owner = holder?.name?.getText?.() ?? null;
8791
+ return owner && member ? `${owner}.${member}` : null;
8792
+ };
8793
+ const declIsReviewedLib = (decl) => !!decl && (declModule(decl) === "<es-lib>" || declIsNodeTypes(decl));
8794
+ const hofCalleeNeverInvokes = (decl, name) => {
8795
+ if ((name && HOF_REMOVAL_NAMES.has(name)) || HOF_REMOVAL_NAMES.has(decl?.name?.getText?.() ?? "")) return true;
8796
+ return declIsReviewedLib(decl) && HOF_OWNED_ELSEWHERE.has(hofOwnerMember(decl) ?? "");
8797
+ };
8798
+ const calleeParamIsGenericSlot = (node, i) => {
8799
+ const sd = checker.getResolvedSignature?.(node)?.declaration;
8800
+ const ps = sd?.parameters;
8801
+ if (!ps?.length || i >= ps.length || !ps[i].type || ps[i].dotDotDotToken) return false;
8802
+ const t = checker.getTypeFromTypeNode(ps[i].type);
8803
+ const isFreeTP = (u) => !!(u.flags & ts.TypeFlags.TypeParameter)
8804
+ && !(checker.getBaseConstraintOfType(u)?.getCallSignatures?.().length > 0);
8805
+ const parts = t.types ?? [t];
8806
+ return parts.some(isFreeTP) && !parts.some((u) => !isFreeTP(u) && u.getCallSignatures?.().length > 0);
8807
+ };
8808
+ // ⟨R1031⟩ reviewed-library callees whose object-literal argument's callable members are INSTALLED, not
8809
+ // invoked: a property descriptor (`get`/`set`) or a descriptor map. Asked of the resolved declaration.
8810
+ const OBJ_ARG_INSTALLS = new Set(["ObjectConstructor.defineProperty", "ObjectConstructor.defineProperties",
8811
+ "ObjectConstructor.create", "Reflect.defineProperty"]);
8812
+ const HOF_REACH = process.env.CANDOR_HOF_REACH ? (k, n) => console.error(`HOF-REACH ${k} ${n}`) : null;
8813
+
8360
8814
  // ---- process.env recognition: the direct dot access (`process.env.KEY`) is the JVM System.getenv twin,
8361
8815
  // but the same environment READ is spelled several other ways that all read silent-pure without help:
8362
8816
  // bracket access (`process.env[k]`), a local const-alias (`const env = process.env; env.KEY`),
@@ -8387,14 +8841,25 @@ const declImportsNodeProcess = (decl) => {
8387
8841
  // downstream consult — the fix in one place, per brief §F1 item 3 ("make the two paths share one
8388
8842
  // authority", not patch the losing copy). Populated by the pre-pass immediately below.
8389
8843
  const processAliasSymbols = new Set();
8390
- const identIsGlobalProcess = (id) => {
8844
+ const identIsGlobalProcess = (id0) => {
8845
+ // ⟨SOUNDNESS R936⟩ through the ONE transparent-wrapper set (R780): `(process as any).argv`, `(<any>process).argv`
8846
+ // and even `(process).argv` were ABSENT because the argv arm handed this the wrapped node and this matched an
8847
+ // Identifier only — the env arm unwrapped first and the argv arm did not. Unwrapping HERE gives every caller
8848
+ // the same answer instead of each remembering to.
8849
+ const id = unwrapArgExpr(id0);
8391
8850
  if (!id) return false;
8851
+ // ⟨SOUNDNESS R936⟩ `require("process")` / `require("node:process")` IS the process object — the module-binding
8852
+ // spelling of the same global (CJS, `--allow-js` trees, and `createRequire`). `require("process").env.HOME` read
8853
+ // nothing (EXECUTED). Keyed on a callee spelled `require` and the literal specifier; a project function that
8854
+ // happens to be called `require` and returns something else is the only false match, and it over-charges.
8855
+ if (ts.isCallExpression(id) && ts.isIdentifier(id.expression) && id.expression.text === "require"
8856
+ && id.arguments.length === 1 && ts.isStringLiteralLike(id.arguments[0])
8857
+ && (id.arguments[0].text === "process" || id.arguments[0].text === "node:process")) return true;
8392
8858
  // `globalThis.process` / `global.process` — the SAME process object reached off the global (isomorphic code:
8393
8859
  // `globalThis.process?.env`, often `(globalThis as any).process.env`). Unwrap parens/`as` casts around the
8394
8860
  // root; the `globalThis`/`global` root must be the ambient global, not a project shadow.
8395
8861
  if (ts.isPropertyAccessExpression(id) && id.name.text === "process") {
8396
- let root = id.expression;
8397
- while (ts.isParenthesizedExpression(root) || ts.isAsExpression(root) || ts.isNonNullExpression(root)) root = root.expression;
8862
+ const root = unwrapArgExpr(id.expression); // R780: `(<any>globalThis).process` is the same object
8398
8863
  if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
8399
8864
  const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
8400
8865
  return !gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
@@ -8423,9 +8888,7 @@ const identIsGlobalProcess = (id) => {
8423
8888
  {
8424
8889
  const isGlobalProcessInitializer = (expr) => {
8425
8890
  if (!expr) return false;
8426
- let e = expr;
8427
- while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
8428
- return identIsGlobalProcess(e); // handles `globalThis.process` / `global.process` / bare ambient `process`
8891
+ return identIsGlobalProcess(unwrapArgExpr(expr)); // R780 // handles `globalThis.process` / `global.process` / bare ambient `process`
8429
8892
  };
8430
8893
  const collectProcessAliases = (node) => {
8431
8894
  if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.initializer) {
@@ -8434,8 +8897,7 @@ const identIsGlobalProcess = (id) => {
8434
8897
  if (sym) processAliasSymbols.add(sym);
8435
8898
  }
8436
8899
  } else if (ts.isVariableDeclaration(node) && node.name && ts.isObjectBindingPattern(node.name) && node.initializer) {
8437
- let root = node.initializer;
8438
- while (ts.isParenthesizedExpression(root) || ts.isAsExpression(root) || ts.isNonNullExpression(root)) root = root.expression;
8900
+ const root = unwrapArgExpr(node.initializer); // R780
8439
8901
  if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
8440
8902
  const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
8441
8903
  if (!gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
@@ -8476,11 +8938,7 @@ const identIsGlobalProcess = (id) => {
8476
8938
  // EXPRESSION — that is the model this adopts, with identity widened from a node to a value.
8477
8939
  //
8478
8940
  // TRANSPARENT: the wrappers that change only the static type, never the value.
8479
- const unwrapEnvTransparent = (e) => {
8480
- while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression(e)
8481
- || ts.isNonNullExpression(e) || ts.isSatisfiesExpression(e))) e = e.expression;
8482
- return e;
8483
- };
8941
+ const unwrapEnvTransparent = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a fourth copy of it
8484
8942
  // `process.env` / `process["env"]` / `globalThis.process?.env` / `(process as any).env` — the global
8485
8943
  // process object's `env` member. The `process` side keeps `identIsGlobalProcess`'s shadow guard, so a
8486
8944
  // project-local `process` (a parameter, a module `const`) matches nothing.
@@ -8536,16 +8994,86 @@ const envValueLeaves = (expr, out = []) => {
8536
8994
  // READ was not even `Unknown` — it was absent).
8537
8995
  // A binding is a `VariableDeclaration` with an identifier name (no initializer = a binding with no env
8538
8996
  // leaf), a plain or logical (`??=`/`||=`/`&&=`) assignment to an identifier, or `{ env } = process` /
8539
- // `{ env: e } = process` (wrappers unwrapped). A PARAMETER is never an alias — callers bind it — so
8540
- // `e = process.env` as a default is charged where the default is evaluated (see the Env arm).
8997
+ // `{ env: e } = process` (wrappers unwrapped).
8998
+ //
8999
+ // ⟨R934/R935⟩ THE SAME QUESTION OVER EVERY STORAGE LOCATION WHOSE BINDINGS THIS PASS CAN ENUMERATE, not
9000
+ // over locals only. The identity above was asked of a VARIABLE; the environment object also travels
9001
+ // through a PARAMETER (R934: `function rd(e) { return e.K }` called as `rd(process.env)` — the read runs
9002
+ // in `rd`'s frame and `rd` was absent) and through a heap FIELD (R935: `const cfg = { env: process.env }`
9003
+ // at module level, read later as `cfg.env.K` by a function that never calls the module initializer —
9004
+ // absent; only `<module>` carried Env). Both are one defect: the alias model stopped at the first
9005
+ // location that was not a local, so the value was lost the moment it was stored anywhere else. The fix
9006
+ // is not a second analysis per location kind; it is the SAME binding table and the SAME two fixpoints
9007
+ // with two more kinds of location in them:
9008
+ //
9009
+ // PARAMETER — its bindings are the arguments at every VISIBLE call site (positional, up to a spread;
9010
+ // a site that omits it binds its default, else `undefined`). It joins MUST only — every visible site
9011
+ // hands it the environment — and NEVER MAY. That asymmetry is pass 2b's ruling, transferred: a
9012
+ // parameter fed the environment at one site and `{}` at another is the DIVERGENT shared-HOF shape,
9013
+ // and charging the callee would pool the read onto the caller that passed `{}` (a fabrication). The
9014
+ // caller that passes the environment is already charged by the Env arm (a call argument is not an
9015
+ // inert consumer), so every gate above the callee still fires — the callee's own scoped gate is the
9016
+ // only one that stays 0, exactly as it does for a divergent HOF. Only functions whose every reference
9017
+ // is a CALL are eligible: a function that escapes as a value (a callback, `.call`, `.bind`, stored)
9018
+ // has bindings this pass cannot see. Methods are eligible only on a class with no heritage that no
9019
+ // project class extends — a dispatched call resolves to the base declaration, and an override's
9020
+ // parameter would otherwise look fed by fewer sites than it is.
9021
+ // PROPERTY — its bindings are the initializer of every VALUE-BEARING declaration (an object-literal
9022
+ // property, a class field, a constructor parameter property — which is bound exactly as its
9023
+ // parameter is) and the right side of every assignment to it. An absent initializer is not a binding:
9024
+ // a field that is `undefined` until it is assigned the environment cannot make a reader read a
9025
+ // DIFFERENT object's keys, and that is the only thing MUST has to rule out. MUST charges a reader Env;
9026
+ // MAY (rebound to something else somewhere) discloses `Unknown[env-maybe-read]`, the local-alias
9027
+ // posture, because unlike a parameter there is no caller in the reader's chain that carries it. A
9028
+ // property declared by an interface or a type literal (`PropertySignature`) is NOT a location here:
9029
+ // any conforming object can be its value, so its bindings are an open set — the residual is stated
9030
+ // in SOUNDNESS, not hidden. A field of a class some project class extends is excluded for the
9031
+ // override reason above.
9032
+ //
9033
+ // Writing the environment INTO a property stays charged where it happens (the Env arm's "store" rule);
9034
+ // this adds the READERS and removes nothing.
8541
9035
  const envAliasSymbols = new Set();
8542
9036
  const envMayAliasSymbols = new Set();
9037
+ // ⟨R935⟩ property locations, kept apart from the local sets so a property name never reaches the
9038
+ // identifier-keyed consumers (2c's `envSourceKind`, the alias-binding inert rule) by accident.
9039
+ const envPropMustSymbols = new Set();
9040
+ const envPropMaySymbols = new Set();
9041
+ // ⟨R935⟩ a property location's identity: its first declaration node (see `memberSymOf` for why not the symbol).
9042
+ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
8543
9043
  {
8544
9044
  const ENV_DESTRUCTURED = Symbol("env-destructured");
8545
- const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED]
9045
+ const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED | { same: symbol }]
8546
9046
  const note = (sym, v) => { if (sym) (bindings.get(sym) ?? bindings.set(sym, []).get(sym)).push(v); };
8547
9047
  const LOGICAL_ASSIGN = new Set([ts.SyntaxKind.EqualsToken, ts.SyntaxKind.QuestionQuestionEqualsToken,
8548
9048
  ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken]);
9049
+ const paramSyms = new Set(); // ⟨R934⟩ parameters with at least one recorded binding
9050
+ const propSyms = new Set(); // ⟨R935⟩ eligible property locations with at least one binding
9051
+ const propIneligible = new Set();
9052
+ // The LOCATION KEY of a member reference: `o.p` / `o["p"]` → the property's first declaration node.
9053
+ // Keyed by DECLARATION, not symbol: an object literal's type is WIDENED when it is bound, and the
9054
+ // widened type carries fresh transient property symbols — `cfg.env` at a read site and `env:` in the
9055
+ // literal are two symbol objects for one declaration (measured: the read never matched the binding).
9056
+ const memberRawSym = (n) => (ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
9057
+ : (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
9058
+ ? checker.getSymbolAtLocation(n.argumentExpression) : null) ?? null;
9059
+ const memberSymOf = (n) => envLocKey(memberRawSym(n));
9060
+ // Is this property symbol a location whose bindings we can enumerate? Every declaration must be
9061
+ // value-bearing, in a project file, and not a member of an extended class.
9062
+ const propEligible = (sym) => {
9063
+ if (!sym || propIneligible.has(sym)) return false;
9064
+ const decls = sym.declarations ?? [];
9065
+ const ok = decls.length > 0 && decls.every((d) => {
9066
+ if (!projectFiles.has(path.resolve(d.getSourceFile().fileName))) return false;
9067
+ if (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) return ts.isObjectLiteralExpression(d.parent);
9068
+ if (ts.isPropertyDeclaration(d)) return !d.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword)
9069
+ && !extendedClassSyms.has(classSymOf(d.parent));
9070
+ if (ts.isParameter(d)) return ts.isConstructorDeclaration(d.parent) && !extendedClassSyms.has(classSymOf(d.parent.parent));
9071
+ return false;
9072
+ });
9073
+ if (!ok) propIneligible.add(sym);
9074
+ return ok;
9075
+ };
9076
+ const noteProp = (sym, v) => { if (propEligible(sym)) { const k = envLocKey(sym); propSyms.add(k); note(k, v); } };
8549
9077
  const collect = (node) => {
8550
9078
  if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
8551
9079
  note(checker.getSymbolAtLocation(node.name), node.initializer ?? null);
@@ -8558,28 +9086,97 @@ const envMayAliasSymbols = new Set();
8558
9086
  }
8559
9087
  } else if (ts.isBinaryExpression(node) && LOGICAL_ASSIGN.has(node.operatorToken.kind) && ts.isIdentifier(node.left)) {
8560
9088
  note(checker.getSymbolAtLocation(node.left), node.right);
9089
+ } else if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
9090
+ && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
9091
+ && (ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left))) {
9092
+ // ⟨R935⟩ an assignment to a property location; a compound operator (`+=`) binds a non-environment.
9093
+ const s = memberRawSym(node.left);
9094
+ if (s) noteProp(s, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null);
9095
+ } else if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
9096
+ const s = checker.getSymbolAtLocation(node.name);
9097
+ if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, node.initializer);
9098
+ } else if (ts.isShorthandPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
9099
+ const s = checker.getSymbolAtLocation(node.name);
9100
+ const v = checker.getShorthandAssignmentValueSymbol(node);
9101
+ if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, v ? { same: v } : null);
9102
+ } else if (ts.isPropertyDeclaration(node) && node.initializer && node.name) {
9103
+ noteProp(checker.getSymbolAtLocation(node.name), node.initializer);
9104
+ } else if (ts.isParameter(node) && ts.isIdentifier(node.name) && ts.isConstructorDeclaration(node.parent)
9105
+ && ts.getModifiers?.(node)?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword
9106
+ || m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword
9107
+ || m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
9108
+ // a constructor parameter property is bound exactly as its parameter is
9109
+ const [ps, fs] = checker.getSymbolsOfParameterPropertyDeclaration(node, node.name.text);
9110
+ if (ps && fs) noteProp(fs, { same: ps });
9111
+ }
9112
+ // ⟨R935⟩ WRITES THE TABLE ABOVE CANNOT NAME: a reflective write into the object (`Object.assign(o, …)`,
9113
+ // `Object.defineProperty`, `Reflect.set`) or a computed-key store `o[k] = v` can rebind ANY of the
9114
+ // object's properties. Every eligible property of the target's type gets a non-environment binding,
9115
+ // so a location written that way can be MAY at best — a reader discloses `Unknown` rather than being
9116
+ // charged `Env` over a value that was replaced off the books (executed: an `Object.assign` rebind read
9117
+ // the replacement, and the first cut of this charged it `Env`).
9118
+ const reflectTarget = ts.isCallExpression(node)
9119
+ && /^(Object\.(assign|defineProperty|defineProperties)|Reflect\.(set|defineProperty))$/.test(
9120
+ (ts.isPropertyAccessExpression(node.expression) ? node.expression.getText() : "").replace(/\s+/g, ""))
9121
+ ? node.arguments[0]
9122
+ : (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
9123
+ && ts.isElementAccessExpression(node.left) && node.left.argumentExpression
9124
+ && !ts.isStringLiteralLike(node.left.argumentExpression) && !ts.isNumericLiteral(node.left.argumentExpression))
9125
+ ? node.left.expression : null;
9126
+ if (reflectTarget) {
9127
+ let props = [];
9128
+ try { props = checker.getTypeAtLocation(reflectTarget)?.getProperties?.() ?? []; } catch { props = []; }
9129
+ for (const ps of props) if (propEligible(ps)) noteProp(ps, null);
8561
9130
  }
8562
9131
  ts.forEachChild(node, collect);
8563
9132
  };
8564
9133
  for (const sf of sources) collect(sf);
8565
- const leafIn = (leaf, set) => isProcessEnvExpr(leaf)
8566
- || (ts.isIdentifier(leaf) && set.has(checker.getSymbolAtLocation(leaf)));
8567
- const hasEnvLeaf = (v, set) => v === ENV_DESTRUCTURED || (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set)));
8568
- // MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY".
9134
+ // ⟨R934⟩ the parameter bindings come from the shared PARAMETER PROVENANCE index above.
9135
+ for (const [ps, vals] of paramArgs) { paramSyms.add(ps); for (const v of vals) note(ps, v); }
9136
+ const leafIn = (leaf, set, propSet) => isProcessEnvExpr(leaf)
9137
+ || (ts.isIdentifier(leaf) && set.has(checker.getSymbolAtLocation(leaf)))
9138
+ || (!!propSet && propSet.has(memberSymOf(leaf)));
9139
+ const hasEnvLeaf = (v, set, propSet) => v === ENV_DESTRUCTURED
9140
+ || (!!v && typeof v === "object" && "same" in v ? set.has(v.same) || propSet?.has(v.same) === true
9141
+ : (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set, propSet))));
9142
+
9143
+
9144
+ // MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS and
9145
+ // PROPERTIES (a property's MAY reads a local's MAY and vice versa). Parameters never join MAY (above).
8569
9146
  for (let changed = true, n = 0; changed && n < 64; n++) {
8570
9147
  changed = false;
8571
9148
  for (const [sym, vals] of bindings) {
8572
- if (!envMayAliasSymbols.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols))) { envMayAliasSymbols.add(sym); changed = true; }
9149
+ if (paramSyms.has(sym)) continue;
9150
+ const isProp = propSyms.has(sym);
9151
+ const set = isProp ? envPropMaySymbols : envMayAliasSymbols;
9152
+ if (!set.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols, envPropMaySymbols))) { set.add(sym); changed = true; }
8573
9153
  }
8574
9154
  }
8575
9155
  // MUST: least fixpoint (from empty) of "every binding has a leaf that is process.env or already MUST" —
8576
- // least, so a cycle that never touches process.env directly stays MAY (the disclosing side).
8577
- for (let changed = true, n = 0; changed && n < 64; n++) {
8578
- changed = false;
9156
+ // least, so a cycle that never touches process.env directly stays MAY (the disclosing side). Locals and
9157
+ // properties start from their MAY sets; parameters are tried directly (MUST-only). A parameter whose
9158
+ // function escapes is dropped before it can join.
9159
+ const paramCands = [...paramSyms].filter((s) => bindings.get(s)?.some((v) => v !== null));
9160
+ let batched = false;
9161
+ const mustTry = () => {
9162
+ let changed = false;
8579
9163
  for (const sym of envMayAliasSymbols) {
8580
- if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols))) { envAliasSymbols.add(sym); changed = true; }
9164
+ if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envAliasSymbols.add(sym); changed = true; }
8581
9165
  }
8582
- }
9166
+ for (const sym of envPropMaySymbols) {
9167
+ if (!envPropMustSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envPropMustSymbols.add(sym); changed = true; }
9168
+ }
9169
+ for (const sym of paramCands) {
9170
+ if (envAliasSymbols.has(sym)) continue;
9171
+ if (!bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) continue;
9172
+ const fsym = paramOwnerOf.get(sym);
9173
+ if (!batched) { batched = true; paramFnEscaped.batch(paramCands.map((p) => paramOwnerOf.get(p)).filter(Boolean)); }
9174
+ if (paramFnEscaped(fsym)) continue;
9175
+ envAliasSymbols.add(sym); changed = true;
9176
+ }
9177
+ return changed;
9178
+ };
9179
+ for (let n = 0; mustTry() && n < 64; n++) { /* to fixpoint */ }
8583
9180
  }
8584
9181
  // True when `id` is an identifier resolving to a confirmed process.env alias local.
8585
9182
  const identIsEnvAlias = (id) => {
@@ -8595,11 +9192,26 @@ const identIsEnvMayAlias = (id) => {
8595
9192
  };
8596
9193
  // The env kind of an expression's VALUE: "env" if some leaf is process.env or a MUST-alias, "may" if
8597
9194
  // some leaf is only a MAY-alias, else null. The one answer the Env arm, 2c and the shared helpers ask.
9195
+ // ⟨R935⟩ a member reference `o.p` / `o["p"]` whose property is an environment location.
9196
+ const envMemberSym = (n) => {
9197
+ if (!(envPropMaySymbols.size > 0)) return null;
9198
+ if (ts.isPropertyAccessExpression(n)) return envLocKey(checker.getSymbolAtLocation(n.name));
9199
+ if (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
9200
+ return envLocKey(checker.getSymbolAtLocation(n.argumentExpression));
9201
+ return null;
9202
+ };
9203
+ const envPropKind = (n) => {
9204
+ const s = envMemberSym(n);
9205
+ if (!s || !envPropMaySymbols.has(s)) return null;
9206
+ return envPropMustSymbols.has(s) ? "env" : "may";
9207
+ };
8598
9208
  const envValueKind = (expr) => {
8599
9209
  let kind = null;
8600
9210
  for (const l of envValueLeaves(expr)) {
8601
9211
  if (isProcessEnvExpr(l) || identIsEnvAlias(l)) return "env";
8602
- if (identIsEnvMayAlias(l)) kind = "may";
9212
+ const pk = envPropKind(l);
9213
+ if (pk === "env") return "env";
9214
+ if (identIsEnvMayAlias(l) || pk === "may") kind = "may";
8603
9215
  }
8604
9216
  return kind;
8605
9217
  };
@@ -8612,8 +9224,21 @@ const envIsAssignOp = (k) => k >= ts.SyntaxKind.FirstAssignment && k <= ts.Synta
8612
9224
  let envAliasNames = null;
8613
9225
  const envRefKind = (node) => {
8614
9226
  if (isProcessEnvExpr(node)) return "env";
9227
+ // ⟨R935⟩ a READ of an environment property location. Not an assignment target, a `delete` operand, a
9228
+ // destructuring target or a `++`/`--` operand: those write the location, they do not read the object.
9229
+ if (envPropMaySymbols.size > 0 && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))) {
9230
+ const pk = envPropKind(node);
9231
+ if (!pk) return null;
9232
+ const p = node.parent;
9233
+ if (p && ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
9234
+ if (p && (ts.isDeleteExpression(p) || ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
9235
+ && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)))) return null;
9236
+ if (isDestructuringAssignTarget(node)) return null;
9237
+ return pk;
9238
+ }
8615
9239
  if (!ts.isIdentifier(node)) return null;
8616
- envAliasNames ??= new Set([...envMayAliasSymbols].map((s) => s.name));
9240
+ // ⟨R934⟩ MUST-parameters are in `envAliasSymbols` but never in the MAY set, so the name filter reads both.
9241
+ envAliasNames ??= new Set([...envMayAliasSymbols, ...envAliasSymbols].map((s) => s.name));
8617
9242
  if (!envAliasNames.has(node.text)) return null;
8618
9243
  const p = node.parent;
8619
9244
  if (!p) return null;
@@ -8628,8 +9253,9 @@ const envRefKind = (node) => {
8628
9253
  && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return null;
8629
9254
  const sym = ts.isShorthandPropertyAssignment(p) && p.name === node
8630
9255
  ? checker.getShorthandAssignmentValueSymbol(p) : checker.getSymbolAtLocation(node);
8631
- if (!sym || !envMayAliasSymbols.has(sym)) return null;
8632
- return envAliasSymbols.has(sym) ? "env" : "may";
9256
+ if (!sym) return null;
9257
+ if (envAliasSymbols.has(sym)) return "env";
9258
+ return envMayAliasSymbols.has(sym) ? "may" : null;
8633
9259
  };
8634
9260
  // ⟨R928/R804⟩ Does the CONSUMER of this env value provably touch no key? Climb through what passes the
8635
9261
  // value on unchanged (the transparent wrappers, both arms of `?:`, either side of `??`/`||`, the right of
@@ -8761,14 +9387,10 @@ const globalBuiltinCallee = (callee) => {
8761
9387
  if (!callee || !ts.isPropertyAccessExpression(callee)) return null;
8762
9388
  const member = callee.name?.text;
8763
9389
  if (!member) return null;
8764
- let owner = callee.expression;
8765
- while (owner && (ts.isParenthesizedExpression(owner) || ts.isAsExpression(owner)
8766
- || ts.isNonNullExpression(owner))) owner = owner.expression;
9390
+ const owner = unwrapArgExpr(callee.expression); // R780
8767
9391
  if (ts.isIdentifier(owner)) return identIsGlobal(owner) ? `${owner.text}.${member}` : null;
8768
9392
  if (!ts.isPropertyAccessExpression(owner) || !owner.name?.text) return null;
8769
- let root = owner.expression;
8770
- while (root && (ts.isParenthesizedExpression(root) || ts.isAsExpression(root)
8771
- || ts.isNonNullExpression(root))) root = root.expression;
9393
+ const root = unwrapArgExpr(owner.expression); // R780
8772
9394
  if (!ts.isIdentifier(root) || !GLOBAL_ROOTS.has(root.text) || !identIsGlobal(root)) return null;
8773
9395
  return `${owner.name.text}.${member}`;
8774
9396
  };
@@ -8805,9 +9427,7 @@ const globalBareCallee = (callee) => {
8805
9427
  if (!callee) return null;
8806
9428
  if (ts.isIdentifier(callee)) return identIsGlobal(callee) ? callee.text : null;
8807
9429
  if (!ts.isPropertyAccessExpression(callee) || !callee.name?.text) return null;
8808
- let root = callee.expression;
8809
- while (root && (ts.isParenthesizedExpression(root) || ts.isAsExpression(root)
8810
- || ts.isNonNullExpression(root))) root = root.expression;
9430
+ const root = unwrapArgExpr(callee.expression); // R780
8811
9431
  // ONLY a global ROOT, never an arbitrary owner: `Object.assign` must not read as the bare global
8812
9432
  // `assign`, and `globalThis.Object.assign` must not read as the bare global `Object` — both are the
8813
9433
  // member helper's business, and answering them here would be the second copy §G exists to prevent.
@@ -9143,8 +9763,95 @@ function visitCalls(node) {
9143
9763
  // never a fabrication. (A LOCAL callee keeps its precise callback-flow below.)
9144
9764
  const calleeName = ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text
9145
9765
  : ts.isIdentifier(node.expression) ? node.expression.text : null;
9146
- if (mod !== "<local>" && calleeName && HOF_INVOKERS.has(calleeName)) {
9147
- node.arguments?.forEach((a0, argIdx) => {
9766
+ // ⟨SOUNDNESS R803⟩ a LISTED name keeps its position map; an UNLISTED non-local callee is admitted at the
9767
+ // positions its signature POSITIVELY declares callable, unless denylisted. See `HOF_REMOVAL_NAMES`.
9768
+ const hofListed = !!calleeName && HOF_INVOKERS.has(calleeName);
9769
+ const hofName = calleeName ?? decl.name?.getText?.() ?? null;
9770
+ const hofOpenAt = (!hofListed && mod !== "<local>" && node.arguments?.length
9771
+ && !hofCalleeNeverInvokes(decl, hofName))
9772
+ ? (i) => calleeParamIsCallable(node, i) === true
9773
+ && !(declIsReviewedLib(decl) && calleeParamIsGenericSlot(node, i)) : null;
9774
+ // ⟨SOUNDNESS R1031⟩ THE SAME RULE ONE LEVEL DOWN: a function reference handed inside an OBJECT
9775
+ // LITERAL. rxjs `of(1).subscribe({ next: h })`, `{ complete: h }`, `{ error: h }`, `{ next }` and
9776
+ // `{ next: o.m.bind(o) }` were ABSENT with `deny Fs <caller>` exit 0 while the bare `subscribe(h)`
9777
+ // read `['Fs']` (EXECUTED: every handler that the observable fires wrote its file). R803 admits a
9778
+ // POSITION its signature declares callable; the observer object's MEMBERS are positions too, and
9779
+ // the signature declares them just as positively — `Partial<Observer<T>>`'s `next` is
9780
+ // `((value: T) => void) | undefined`. So each property whose value is a reference, at a member the
9781
+ // parameter's (non-callable) type arms declare callable, is handed to the SAME per-argument arm as
9782
+ // a bare reference — one implementation of "a reference the callee may invoke", not a second copy.
9783
+ // Inline arrows and methods in the literal were never dropped (they are structural units, charged
9784
+ // lexically). Same admission as R803's open path: an unlisted non-local callee off the denylist; an
9785
+ // `any`/`unknown`-typed parameter or member admits nothing (the R803 residual, unchanged).
9786
+ const objArgRefs = (argIdx, lit, depth = 0) => {
9787
+ if (depth > 2) return [];
9788
+ // The parameter's DECLARED type, not the call's instantiation of it: `Object.assign(p, { unsubscribe })`
9789
+ // instantiates `U` as `{ unsubscribe: () => void }`, which reads callable only because the CALL made it
9790
+ // so — the signature declares nothing about invoking it (measured: trpc's `Unpromise.then` charged
9791
+ // `callback:unsubscribe` until this read the declaration). A generic slot names no members, so it
9792
+ // admits nothing; `Partial<Observer<T>>` declares `next` callable whatever `T` is.
9793
+ let sd; try { sd = checker.getResolvedSignature?.(node)?.declaration; } catch { sd = null; }
9794
+ const dps = sd?.parameters;
9795
+ if (!dps?.length) return [];
9796
+ const dp = dps[Math.min(argIdx, dps.length - 1)];
9797
+ if (argIdx >= dps.length && !dp.dotDotDotToken) return [];
9798
+ if (!dp.type) return [];
9799
+ let pt; try { pt = checker.getTypeFromTypeNode(dp.type); } catch { pt = null; }
9800
+ if (dp.dotDotDotToken && pt) pt = checker.getIndexTypeOfType?.(pt, ts.IndexKind.Number) ?? null; // `...xs: T[]` → T
9801
+ return objLitRefsFor(pt, lit, depth);
9802
+ };
9803
+ const callableT = (t) => !!t && (t.getCallSignatures?.().length > 0 || (t.types ?? []).some((u) => u.getCallSignatures?.().length > 0));
9804
+ const objLitRefsFor = (pt, lit, depth) => {
9805
+ if (!pt || (pt.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) return [];
9806
+ const arms = (pt.types ?? [pt]).filter((u) => !(u.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Any | ts.TypeFlags.Unknown))
9807
+ && !(u.getCallSignatures?.().length > 0));
9808
+ const out = [];
9809
+ for (const pr of lit.properties) {
9810
+ const nm = pr.name && (ts.isIdentifier(pr.name) || ts.isStringLiteralLike(pr.name)) ? pr.name.text : null;
9811
+ if (!nm) continue;
9812
+ const memberTypes = arms.map((u) => { const ms = u.getProperty?.(nm); return ms ? checker.getTypeOfSymbolAtLocation(ms, lit) : null; }).filter(Boolean);
9813
+ if (ts.isShorthandPropertyAssignment(pr)) {
9814
+ if (memberTypes.some(callableT)) out.push({ shorthand: pr });
9815
+ continue;
9816
+ }
9817
+ if (!ts.isPropertyAssignment(pr)) continue; // a method / accessor is a structural unit already
9818
+ const v = unwrapArgExpr(pr.initializer);
9819
+ if (ts.isObjectLiteralExpression(v)) { for (const mt of memberTypes) out.push(...objLitRefsFor(mt, v, depth + 1)); continue; }
9820
+ if (memberTypes.some(callableT)) out.push({ ref: pr.initializer });
9821
+ }
9822
+ return out;
9823
+ };
9824
+ if (mod !== "<local>" && (hofListed || hofOpenAt)) {
9825
+ const hofArgArm = (a0, argIdx) => {
9826
+ // ⟨R1031⟩ an object literal at an admitted callee: its referenced members are the arguments.
9827
+ if (!hofListed) {
9828
+ const lit = unwrapArgExpr(a0);
9829
+ if (lit && ts.isObjectLiteralExpression(lit)) {
9830
+ // A property DESCRIPTOR installs its `get`/`set` and invokes neither; the access sites are
9831
+ // edged to the installed accessor by `definePropAccessors`. Charging the definer would be the
9832
+ // over-charge R803's generic-slot denylist exists for, one level down.
9833
+ if (declIsReviewedLib(decl) && OBJ_ARG_INSTALLS.has(hofOwnerMember(decl) ?? "")) return;
9834
+ for (const r of objArgRefs(argIdx, lit)) {
9835
+ if (r.ref) { HOF_REACH?.("objref", `${hofName}`); hofArgArm(r.ref, -1 - argIdx); continue; }
9836
+ // `{ next }` — the shorthand's VALUE symbol, followed one alias hop to what it holds.
9837
+ const vs = checker.getShorthandAssignmentValueSymbol(r.shorthand);
9838
+ const vd = vs?.valueDeclaration;
9839
+ HOF_REACH?.("objref", `${hofName}`);
9840
+ if (vd && nodeName.get(vd)) rec.edges.add(nodeName.get(vd));
9841
+ else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && !ts.isArrowFunction(vd.initializer)
9842
+ && !ts.isFunctionExpression(vd.initializer)) hofArgArm(vd.initializer, -1 - argIdx);
9843
+ else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && nodeName.get(vd.initializer)) rec.edges.add(nodeName.get(vd.initializer));
9844
+ else if (!vd || ts.isParameter(vd) || ts.isVariableDeclaration(vd) || ts.isBindingElement(vd)) {
9845
+ rec.direct.add("Unknown"); rec.why.add(`callback:${r.shorthand.name.text.slice(0, 40)}`);
9846
+ }
9847
+ }
9848
+ return;
9849
+ }
9850
+ }
9851
+ // ⟨R1031⟩ a member reference re-entering the arm carries a NEGATIVE index: it is admitted by its
9852
+ // member's declared type above, and no position map or signature lookup applies to it.
9853
+ const memberRef = argIdx < 0;
9854
+ if (!memberRef && !hofListed && !hofOpenAt(argIdx)) return;
9148
9855
  // R947 — a TYPE-ONLY wrapper is the same reference: `xs.forEach(fs.unlinkSync as any)`
9149
9856
  // was dropped by the id/property-access gate below (no edge, no κ, no disclosure) while the bare
9150
9857
  // spelling was not. `as`/`!`/`satisfies`/parentheses change no runtime value.
@@ -9159,7 +9866,7 @@ function visitCalls(node) {
9159
9866
  // the same over-charge the by-reference arm below was fixed for and this arm was not.
9160
9867
  // …but it must be the NEGATIVE test, not `!hofInvokesArg`: an unresolved or loosely-typed
9161
9868
  // callee signature is not a licence to drop a bound callback. See `hofArgIsNeverCallback`.
9162
- if (hofArgIsNeverCallback(calleeName, argIdx, node)) return;
9869
+ if (hofListed && hofArgIsNeverCallback(calleeName, argIdx, node)) return;
9163
9870
  const bound = unwrapBind(a);
9164
9871
  if (bound) {
9165
9872
  const bref = bound.ref;
@@ -9212,7 +9919,8 @@ function visitCalls(node) {
9212
9919
  if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a)) return;
9213
9920
  const d2 = realDecl(checker.getSymbolAtLocation(a));
9214
9921
  const t = (d2 && nodeName.get(d2)) || resolveFnRefUnit(a); // pin direct fn OR a local alias chain
9215
- if (t) { rec.edges.add(t); return; } // resolvable named/local callback — keep its analyzed effect
9922
+ if (t) { if (!hofListed) HOF_REACH?.("edge", `${hofName}->${t}`);
9923
+ rec.edges.add(t); return; } // resolvable named/local callback — keep its analyzed effect
9216
9924
  // A DEPENDENCY function passed BY REFERENCE — `xs.forEach(depWrite)`, `setTimeout(dep.tick, 0)`.
9217
9925
  // The invoking HOF calls it, so its effects are reachable here, and the dependency's report
9218
9926
  // holds them under `<pkg>#depWrite`. Guard (2) below reasons that a ref resolving to a concrete
@@ -9238,7 +9946,7 @@ function visitCalls(node) {
9238
9946
  // through the κ/invisible channel, so blanket-Unknown here would over-disclose them.
9239
9947
  // (3) CALLABILITY — `argIsCallable` (has a call signature, or `any`/`unknown`/unconstrained
9240
9948
  // generic that COULD hold a function).
9241
- if (!hofInvokesArg(calleeName, argIdx, node)) return;
9949
+ if (hofListed && !hofInvokesArg(calleeName, argIdx, node)) return;
9242
9950
  // ⟨SOUNDNESS R558⟩ …and BEFORE the by-reference dependency charge below, without returning:
9243
9951
  // a FOREIGN interface member named as a value needs BOTH the visible-implementor join and
9244
9952
  // the `invisible`/ledger disclosure `chargeExternalDecl` already gives it, and the two
@@ -9253,6 +9961,7 @@ function visitCalls(node) {
9253
9961
  // dep VALUE with a call signature, passes. Guard (1) exists for exactly this shape and its own
9254
9962
  // comment names the case (`path.reduce(fn, obj)`); the new arm simply ran before it.
9255
9963
  if (d2 && !declIsLocal(d2) && argIsCallable(a)) {
9964
+ if (!hofListed) HOF_REACH?.("dep", `${hofName}->${a.getText().slice(0, 40)}`);
9256
9965
  // R947 — A κ-CLASSIFIED BUILTIN PASSED BY REFERENCE. `chargeExternalDecl` is the
9257
9966
  // dependency funnel and never asks κ, and `disclosureTail` treats a κ-KNOWN member as covered, so
9258
9967
  // `xs.forEach(fs.unlinkSync)`, `setTimeout(fs.unlinkSync, 0, p)`, `.then(cp.execSync)` and
@@ -9275,11 +9984,24 @@ function visitCalls(node) {
9275
9984
  const holderIsProjectValue = d2 && (ts.isParameter(d2) || ts.isVariableDeclaration(d2) || ts.isBindingElement(d2))
9276
9985
  && projectFiles.has(path.resolve(d2.getSourceFile().fileName));
9277
9986
  if (!(holderIsProjectValue || !d2)) return;
9987
+ // ⟨SOUNDNESS R803⟩ a newly admitted callee handed its OWN caller's parameter (`reg(e, h) { e.on("y", h) }`)
9988
+ // is the callback flow's question, not an opaque value: the parameter is INVOKED, so pass 2b resolves it
9989
+ // to what every visible call site passed (an edge — `deny Fs` on the caller of `reg` fires) and keeps the
9990
+ // honest `callback:param#i` where a call site is opaque or none is visible. The listed names keep their
9991
+ // older `callback:<name>` disclosure — unchanged here so this fix removes nothing it did not add.
9992
+ if (!hofListed && d2 && ts.isParameter(d2) && d2.parent && nodeName.get(d2.parent) && argIsCallable(a)) {
9993
+ const ownerUnit = nodeName.get(d2.parent);
9994
+ HOF_REACH?.("param", `${hofName}->${a.getText().slice(0, 40)}`);
9995
+ (paramInvokes.get(ownerUnit) ?? paramInvokes.set(ownerUnit, new Set()).get(ownerUnit)).add(d2.parent.parameters.indexOf(d2));
9996
+ return;
9997
+ }
9278
9998
  if (argIsCallable(a)) {
9999
+ if (!hofListed) HOF_REACH?.("opaque", `${hofName}->${a.getText().slice(0, 40)}`);
9279
10000
  rec.direct.add("Unknown");
9280
10001
  rec.why.add(`callback:${a.getText().replace(/\s+/g, "").slice(0, 40)}`); // opaque callable invoked by a sync HOF — canonical `callback:`
9281
10002
  }
9282
- });
10003
+ };
10004
+ node.arguments?.forEach(hofArgArm);
9283
10005
  }
9284
10006
  // `fn.call(thisArg, …)` / `fn.apply(thisArg, args)` INVOKE the receiver function reference, and
9285
10007
  // `Reflect.apply(fn, …)` / `Reflect.construct(Ctor, …)` invoke their FIRST ARGUMENT. The resolved
@@ -9317,6 +10039,11 @@ function visitCalls(node) {
9317
10039
  if ((m === "call" || m === "apply") && recvText !== "Reflect") invokedRef = recv;
9318
10040
  else if (recvText === "Reflect" && (m === "apply" || m === "construct"))
9319
10041
  invokedRef = (node.arguments ?? [])[0] ?? null;
10042
+ // ⟨SOUNDNESS R780⟩ `(k.m as F).call(k)`, `(f satisfies F).call(t)` and `Reflect.apply(f as F, …)` invoke what
10043
+ // the bare spelling invokes. A single wrapped reference is not a CHOICE, so R955's leaves (which do unwrap)
10044
+ // returned null and the id/property gate below saw a ParenthesizedExpression: no edge, no `Unknown`, the
10045
+ // caller ABSENT (EXECUTED, `fx/r780`).
10046
+ invokedRef = unwrapArgExpr(invokedRef);
9320
10047
  // ⟨SOUNDNESS R587⟩ …and the ELEMENT-ACCESS spelling of the invoked reference, on the same
9321
10048
  // grounds as the HOF-ref arm above: `i["roll"].call(null, n)` invokes exactly what
9322
10049
  // `i.roll.call(null, n)` invokes. Measured ABSENT at `6a639e6` in every cell
@@ -9430,8 +10157,7 @@ function visitCalls(node) {
9430
10157
  const recvExpr = (ts.isPropertyAccessExpression(node.expression)
9431
10158
  || ts.isElementAccessExpression(node.expression)) ? node.expression.expression : null;
9432
10159
  const rootClass = localReceiverClass(recvExpr);
9433
- const direct = rootClass ? allOverrides.filter((om) =>
9434
- ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
10160
+ const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
9435
10161
  const overrides = isSuperReceiver(recvExpr) ? [] : memberDispatchBodies(decl, rootClass);
9436
10162
  if (isSuperReceiver(recvExpr)) probeR871("super-call", decl, direct.length);
9437
10163
  else if (direct.length <= CHA_FANOUT_LIMIT) probeR871("class", decl, overrides.length - direct.length);
@@ -9760,6 +10486,22 @@ function visitCalls(node) {
9760
10486
  rec.direct.add("Unknown");
9761
10487
  rec.why.add(`native:Storage.${name}`);
9762
10488
  }
10489
+ // SOUNDNESS R241 — THE REST OF THE BROWSER'S PERSISTENT STORES AND NAVIGATION, which the comment
10490
+ // above named as "untouched and still pure" (the documented-limitation shape: writing it down is
10491
+ // what stopped it being measured). IndexedDB, the Cache API and the Origin Private File System
10492
+ // persist across sessions exactly as `Storage` does, and `location.assign`/`replace`/`reload`
10493
+ // navigate (a request leaves the page). All six of the row's spellings read ABSENT under
10494
+ // `lib: ["ES2022","DOM"]`, while `localStorage.setItem` on the same tree read `Unknown[native:…]`.
10495
+ // Same answer as `Storage`, for the same reason: the WHOLE interface (the denylist direction — an
10496
+ // unlisted member would be silent), `Unknown` because the backing store / navigation target is not
10497
+ // modelled, keyed on the RESOLVED DECLARATION'S PARENT so a project's own `class Cache` resolves
10498
+ // `<local>` and never reaches this arm. Reach on the (Node/server) roster is zero by construction;
10499
+ // the price is paid only by code that already touches these hosts.
10500
+ if (BROWSER_STORE_IFACES.has(parent)
10501
+ || (parent === "Location" && (name === "assign" || name === "replace" || name === "reload"))) {
10502
+ rec.direct.add("Unknown");
10503
+ rec.why.add(`native:${parent}.${name}`);
10504
+ }
9763
10505
  // `new EventSource(url)` / `new WebSocket(url)`: the constructor is declared on an anonymous
9764
10506
  // `declare var` object type (symbol `__type`, no usable parent name), but reaching the es-lib
9765
10507
  // branch already proves the ctor resolved to lib.dom (not a project class shadowing the name),
@@ -9997,6 +10739,22 @@ function visitCalls(node) {
9997
10739
  && (ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
9998
10740
  && receiverIsProvenNonNetworkStream(node.expression.expression))
9999
10741
  { eff = null; effSuppressed = true; }
10742
+ // ⟨SOUNDNESS R966⟩ A FRAMEWORK'S OWN `listen` IS AN ACCEPT. `NET_ACCEPTING` is asked only of a call κ
10743
+ // classified `Net`, and κ classifies node's servers — so express `app.listen(p)` (declared in
10744
+ // express-serve-static-core), koa `app.listen(p)` and fastify `f.listen({port})` reached neither the effect
10745
+ // nor the accept mark: beside `net.connect(80, "ok.example")`, `allow Net in <fn> ok.example` exited 0
10746
+ // over a server answering anyone (EXECUTED: a client got the handler's reply). An inclusion list decided a
10747
+ // fail-closed question (R781's lesson), so this is the fail-closed reading: a DEPENDENCY member named
10748
+ // `listen` that κ did not answer is a network accept — `Net`, and `netAccepting` marks it `incomplete`.
10749
+ // The failure direction is an over-charge on a non-network `listen`; the one family known to be that is
10750
+ // denylisted by package (a JSON-RPC `connection.listen()` starts reading a transport whose OWN
10751
+ // construction decides whether it is a socket). Measured reach on the pinned roster: 1 unclassified
10752
+ // dependency `listen` (nest's FastifyAdapter), 0 non-network. Not seen: hapi `server.start()`
10753
+ // (`start` is p-queue's and repl's too), Bun.serve/Deno.serve (no types in any roster tree).
10754
+ if (!eff && !effSuppressed && member === "listen" && dependencyListenIsAccept(mod, decl)) {
10755
+ eff = "Net";
10756
+ if (process.env.CANDOR_R966_REACH) console.error(`R966-REACH ${mod}.listen`);
10757
+ }
10000
10758
  if (eff) {
10001
10759
  rec.direct.add(eff);
10002
10760
  // SPEC §2 `fs` — refine an Fs we just PROVED with the direction its verb implies. DIRECT only
@@ -10327,7 +11085,16 @@ function visitCalls(node) {
10327
11085
  // `const [,,x] = process.argv`, or handing it to a parser all reach the same process-startup state.
10328
11086
  // Marked on the expression itself so every idiom counts without enumerating them.
10329
11087
  if (isProcessArgvExpr(node)) markEnv();
11088
+ // ⟨R934⟩ a DESTRUCTURED parameter that every visible call site hands the environment reads its keys
11089
+ // as the frame is entered — the destructuring IS the read, in this function's frame.
11090
+ if (ts.isParameter(node) && ts.isObjectBindingPattern(node.name) && envAliasSymbols.has(node)) { markEnv(); ENVLOC_REACH?.("param-pattern env"); }
10330
11091
  const kind = envRefKind(node);
11092
+ if (kind && ENVLOC_REACH && !envConsumerInert(node)) {
11093
+ const vd = ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.valueDeclaration : null;
11094
+ const where = `${path.relative(process.cwd(), node.getSourceFile().fileName)}:${node.getStart() >= 0 ? node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1 : 0}`;
11095
+ if (vd && ts.isParameter(vd)) ENVLOC_REACH(`param ${kind} ${where}`);
11096
+ else if (!ts.isIdentifier(node) && !isProcessEnvExpr(node)) ENVLOC_REACH(`prop ${kind} ${where}`);
11097
+ }
10331
11098
  if (kind && !envConsumerInert(node)) {
10332
11099
  if (kind === "env") markEnv();
10333
11100
  else {
@@ -10368,6 +11135,24 @@ function visitCalls(node) {
10368
11135
  && isWebStorageExpr(node.expression)) {
10369
11136
  markStore(memberLabel(node));
10370
11137
  }
11138
+ // SOUNDNESS R241 — the two PROPERTY spellings of the same host boundary, which no call arm sees: the
11139
+ // cookie jar (`document.cookie`, read OR written — it is a persistent store, `Storage`'s posture) and a
11140
+ // WRITE to any `Location` property (`location.href = u` navigates; so do `pathname`/`search`/`hash`).
11141
+ // lib.dom declares both as DATA properties, so the accessor arm finds nothing either. Keyed on the
11142
+ // resolved declaration living in the TypeScript lib and on its declaring interface, never on text.
11143
+ if (ts.isPropertyAccessExpression(node)) {
11144
+ const hsym = checker.getSymbolAtLocation(node.name);
11145
+ const hdecl = (hsym?.declarations ?? []).find((d) => /typescript\/lib\/lib\..*\.d\.ts$/.test(path.resolve(d.getSourceFile().fileName).replace(/\\/g, "/")));
11146
+ const iface = hdecl?.parent && ts.isInterfaceDeclaration(hdecl.parent) ? hdecl.parent.name.text : null;
11147
+ const pn = node.name.text;
11148
+ const hp = node.parent;
11149
+ const written = (hp && ts.isBinaryExpression(hp) && hp.left === node && envIsAssignOp(hp.operatorToken.kind))
11150
+ || isDestructuringAssignTarget(node);
11151
+ if ((iface === "Document" && pn === "cookie") || (iface === "Location" && written)) {
11152
+ const owner = enclosing(node);
11153
+ if (owner) { const r = fns.get(owner); r.direct.add("Unknown"); r.why.add(`native:${iface}.${pn}`); }
11154
+ }
11155
+ }
10371
11156
  else if (ts.isVariableDeclaration(node) && node.name && ts.isObjectBindingPattern(node.name)
10372
11157
  && node.initializer && isWebStorageExpr(node.initializer)) {
10373
11158
  markStore("<destructure>");
@@ -10788,6 +11573,36 @@ function visitCalls(node) {
10788
11573
  && p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
10789
11574
  const recordKind = (kind) => {
10790
11575
  const hits = accessorsAt(node, kind);
11576
+ // ⟨R246/R1039⟩ THE RESIDUE: a WRITE (R246) or READ (R1039) through a receiver whose static type is
11577
+ // `any`/`unknown` and whose value neither the assertion look-through nor the parameter provenance could
11578
+ // name. Recorded here; whether it is disclosed is decided after the fixpoint, when it is known which
11579
+ // project accessors of that kind have effects.
11580
+ if (!(hits && hits.length)) {
11581
+ let rt1; try { rt1 = checker.getTypeAtLocation(node.expression); } catch { rt1 = null; }
11582
+ if (anyishType(rt1) && !receiverValueTypes(node.expression).length) {
11583
+ const owner1 = enclosing(node);
11584
+ const nm1 = ts.isPropertyAccessExpression(node) ? node.name.text
11585
+ : (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
11586
+ if (owner1) anyRecvSetResidue.push({ owner: owner1, name: nm1, kind });
11587
+ }
11588
+ }
11589
+ if (R246_REACH) {
11590
+ let rt0; try { rt0 = checker.getTypeAtLocation(node.expression); } catch { rt0 = null; }
11591
+ if (anyishType(rt0) || peelAssertions(node.expression) !== node.expression) {
11592
+ const vts = receiverValueTypes(node.expression);
11593
+ const nm = ts.isPropertyAccessExpression(node) ? node.name.text
11594
+ : (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
11595
+ if (hits && hits.length && vts.length) R246_REACH(`resolved ${kind}`);
11596
+ else if (anyishType(rt0) && !vts.length) {
11597
+ projAccessorNames ??= (() => { const m = { get: new Set(), set: new Set() };
11598
+ const v = (n) => { if ((ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n)) && n.name && !ts.isComputedPropertyName(n.name))
11599
+ (ts.isGetAccessorDeclaration(n) ? m.get : m.set).add(n.name.getText()); ts.forEachChild(n, v); };
11600
+ for (const sf of sources) v(sf); return m; })();
11601
+ const could = nm == null ? projAccessorNames[kind].size > 0 : projAccessorNames[kind].has(nm);
11602
+ R246_REACH(`residue ${kind} ${nm == null ? "dyn" : "named"} ${could ? "could-hit-project-accessor" : "no-project-accessor"}`);
11603
+ }
11604
+ }
11605
+ }
10791
11606
  if (hits && hits.length) {
10792
11607
  const owner = enclosing(node);
10793
11608
  if (!owner) return;
@@ -10864,7 +11679,10 @@ function visitCalls(node) {
10864
11679
  // the four symbol rows below); what changed is that the other spelling now asks the same code.
10865
11680
  const keyT = ts.isElementAccessExpression(node) && node.argumentExpression
10866
11681
  ? checker.getTypeAtLocation(node.argumentExpression) : null;
10867
- for (const prop of (rt?.getProperties?.() ?? [])) {
11682
+ // R246 — the VALUE's types too: `(s as any)[k] = v`, or `o[k] = v` with `o: any` fed an `S`.
11683
+ const rprops = [...(rt?.getProperties?.() ?? [])];
11684
+ for (const vt of receiverValueTypes(node.expression)) rprops.push(...(vt.getProperties?.() ?? []));
11685
+ for (const prop of rprops) {
10868
11686
  if (!accessorsFromSym(prop, kind).length) continue;
10869
11687
  if (!keyCouldNameAccessor(prop, keyT)) continue;
10870
11688
  const owner = enclosing(node);
@@ -11282,15 +12100,24 @@ for (const sf of sources) visitCalls(sf);
11282
12100
  })(cls);
11283
12101
  for (const dec of decorators) {
11284
12102
  add(localBodiedUnit(dec), "deco"); // the application
11285
- let e = dec.expression;
11286
- while (ts.isParenthesizedExpression(e)) e = e.expression;
12103
+ const e = unwrapArgExpr(dec.expression); // R780
11287
12104
  if (ts.isCallExpression(e)) {
11288
12105
  add(localBodiedUnit(e), "deco"); // the factory call
11289
12106
  add(`${moduleOf(e.getSourceFile())}.${decoratorArgLocal(e)}`, "arg"); // ⟨R944⟩ the ONE spelling
11290
12107
  }
11291
12108
  }
11292
12109
  for (const m of cls.members ?? [])
11293
- if (ts.isClassStaticBlockDeclaration(m)) add(staticBlockQual(m), "static");
12110
+ if (ts.isClassStaticBlockDeclaration(m)
12111
+ || (ts.isPropertyDeclaration(m) && m.initializer && (ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)))
12112
+ add(staticBlockQual(m), "static"); // R815: a static field initialiser shares the block's unit
12113
+ // R815: the `extends <expr>` unit — run by the definer (here) and reached by every construction (`super`).
12114
+ const ctorQ = nodeName.get(cls);
12115
+ const herQ = ctorQ && ctorQ.endsWith(".constructor") ? ctorQ.replace(/\.constructor$/, ".<heritage>") : null;
12116
+ // Wired only when the expression attributed something: a bare `extends Base` mints the unit (a reference was
12117
+ // attributed) and carries nothing, and an edge to an empty unit is a `calls` entry on every subclass for no answer.
12118
+ const herR = herQ && fns.get(herQ);
12119
+ if (herR && ["direct", "edges", "why", "blind", "hosts", "paths", "cmds", "incomplete", "dispatch", "tables"]
12120
+ .some((k) => herR[k]?.size)) { add(herQ, "heritage"); fns.get(ctorQ)?.edges.add(herQ); }
11294
12121
  if (!targets.length) continue;
11295
12122
  const from = enclosing(cls.parent);
11296
12123
  const rec = from && fns.get(from);
@@ -11797,6 +12624,52 @@ const inferred = new Map([...fns.keys()].map((k) => [k, new Set(fns.get(k).direc
11797
12624
  if (!queued.has(c)) { queued.add(c); queue.push(c); }
11798
12625
  }
11799
12626
  }
12627
+ // ⟨SOUNDNESS R246⟩ THE HEDGE FOR WHAT RESOLUTION CANNOT REACH. A write `o.name = v` / `o[k] = v` through a
12628
+ // receiver typed `any`/`unknown` whose value neither an assertion nor a visible call site names — an escaped
12629
+ // function's parameter, an exported one with no visible caller, an `any` field or return — can still land
12630
+ // on a project SETTER (EXECUTED: `[new Sx()].forEach(w)` with `w(o: any) { o.token = v }` ran Sx's setter, and
12631
+ // `w` was ABSENT, `deny Fs`/`deny Unknown` both exit 0). Which object it is cannot be known, so this
12632
+ // DISCLOSES rather than charges: `Unknown[reflect:accessor:any-receiver]` on the writer, and only when the
12633
+ // project declares a setter that could be the one — the SAME name (or any string-named setter for an
12634
+ // unpinnable key) — AND that setter's body has an effect. A setter-free or pure-setter project pays nothing.
12635
+ // Priced on the 28-entry roster (see the commit for the measured count — a handful of units, <0.1%).
12636
+ // SOUNDNESS R1039 — THE READ SIDE, SAME RULE: `o.name` / `o[k]` through such a receiver can run a project
12637
+ // GETTER (EXECUTED: an escaped `r(o: any) { return o.token }` handed a class whose `get token()` writes a
12638
+ // file — `r` ABSENT, `deny Unknown`/`deny Fs Unknown` exit 0 on v0.40.0). Same admission: a getter WITH A
12639
+ // BODY and an EFFECT that the read could name (the same name, or any string-named one for an unpinnable key).
12640
+ if (anyRecvSetResidue.length) {
12641
+ const accUnits = { get: new Map(), set: new Map() }; // kind -> name -> [unit]
12642
+ const visitS = (n) => {
12643
+ // An accessor WITH A BODY: a body-less declaration (a project typings stub) is already its own `native:`
12644
+ // disclosure, and arming every `any` write on it measured as pure noise (typeorm's bson `ObjectId.set id`).
12645
+ if ((ts.isSetAccessorDeclaration(n) || ts.isGetAccessorDeclaration(n)) && n.body && n.name && !ts.isComputedPropertyName(n.name)) {
12646
+ const u = nodeName.get(n);
12647
+ const m = ts.isSetAccessorDeclaration(n) ? accUnits.set : accUnits.get;
12648
+ if (u) { const k = n.name.getText(); (m.get(k) ?? m.set(k, []).get(k)).push(u); }
12649
+ }
12650
+ ts.forEachChild(n, visitS);
12651
+ };
12652
+ for (const sf of sources) visitS(sf);
12653
+ const effectful = (u) => (inferred.get(u)?.size ?? 0) > 0;
12654
+ const anyEffectful = { get: [...accUnits.get.values()].flat().some(effectful), set: [...accUnits.set.values()].flat().some(effectful) };
12655
+ const hedged = new Set();
12656
+ for (const { owner, name, kind } of anyRecvSetResidue) {
12657
+ const hit = name == null ? anyEffectful[kind] : (accUnits[kind].get(name) ?? []).some(effectful);
12658
+ if (!hit || !fns.has(owner)) continue;
12659
+ const rec = fns.get(owner);
12660
+ rec.direct.add("Unknown"); rec.why.add("reflect:accessor:any-receiver");
12661
+ hedged.add(owner);
12662
+ }
12663
+ // propagate the added `Unknown` to every transitive caller (the fixpoint above has already run)
12664
+ const queue = [...hedged];
12665
+ const seenU = new Set(queue);
12666
+ for (let head = 0; head < queue.length; head++) {
12667
+ const u = queue[head];
12668
+ inferred.get(u)?.add("Unknown");
12669
+ for (const c of callersOf.get(u) ?? [])
12670
+ if (!seenU.has(c) && !inferred.get(c)?.has("Unknown")) { seenU.add(c); queue.push(c); }
12671
+ }
12672
+ }
11800
12673
  // `fsKinds` joins the propagated surfaces: kinds TRAVEL the call graph (a caller that transitively only
11801
12674
  // writes IS a writer), and the "?" poison travels with them so a caller of an undetermined-kind function
11802
12675
  // inherits the SUPPRESSION rather than a half-answer. Pinned by conformance PART 31.