candor-ts 0.40.0 → 0.40.2

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 (4) hide show
  1. package/README.md +14 -0
  2. package/package.json +6 -3
  3. package/policy.mjs +8 -0
  4. package/scan.mjs +1356 -108
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
 
@@ -1645,11 +1653,60 @@ if (NO_SOURCES) {
1645
1653
  // types). Resolved via the module system, NOT a fixed relative path — npm HOISTS dependencies, so
1646
1654
  // in an npx/install tree @types/node sits BESIDE candor-ts, not inside it (the second probe's
1647
1655
  // catch). The TARGET's own @types win when present.
1656
+ // ⟨SOUNDNESS R1062⟩ THE ENGINE'S OWN TYPE PACKAGES ARE PART OF THE ENGINE. The compiler (and its `lib.*.d.ts`),
1657
+ // `@types/node` (the fallback root below) and `undici-types` (which `@types/node` re-exports `fetch`/`WebSocket`
1658
+ // from) decide resolutions, so a different version is, in principle, a different engine. A fresh install of the
1659
+ // published v0.40.1 resolved `@types/node` 25.9.9 against a tested 25.9.2 — MEASURED to move nothing on vitest
1660
+ // (the four-row difference first blamed on it was the type-ROOT leak below), and unmeasured everywhere else. The
1661
+ // package now pins them exactly and ships `npm-shrinkwrap.json`; this note covers the installs that ignore a
1662
+ // shrinkwrap (pnpm, yarn) or a hand-edited tree. Advisory: an untested version is not a wrong verdict, but it is
1663
+ // not the one this build's tests measured, and the operator should be told.
1664
+ {
1665
+ try {
1666
+ const lockPath = ["npm-shrinkwrap.json", "package-lock.json"].map((f) => path.join(ENGINE_DIR, f)).find((f) => fs.existsSync(f));
1667
+ if (lockPath) {
1668
+ const lock = JSON.parse(fs.readFileSync(lockPath, "utf8")).packages ?? {};
1669
+ const req = createRequire(path.join(ENGINE_DIR, "scan.mjs"));
1670
+ const loaded = (name, from) => {
1671
+ try { return JSON.parse(fs.readFileSync(createRequire(from).resolve(`${name}/package.json`), "utf8")).version; }
1672
+ catch { return null; }
1673
+ };
1674
+ const nodeTypesPkg = (() => { try { return req.resolve("@types/node/package.json"); } catch { return null; } })();
1675
+ const seen = [["typescript", ts.version], ["@types/node", loaded("@types/node", path.join(ENGINE_DIR, "scan.mjs"))],
1676
+ ["undici-types", nodeTypesPkg ? loaded("undici-types", nodeTypesPkg) : null]];
1677
+ for (const [name, got] of seen) {
1678
+ const want = lock[`node_modules/${name}`]?.version;
1679
+ if (want && got && want !== got)
1680
+ console.error(`candor-ts: note — this install loaded ${name} ${got}; candor-ts ${PKG_VERSION} was tested `
1681
+ + `with ${want}, and type packages change verdicts (SOUNDNESS R1062). Reinstall with npm, which honours the `
1682
+ + `shipped npm-shrinkwrap.json.`);
1683
+ }
1684
+ }
1685
+ } catch { /* advisory only */ }
1686
+ }
1648
1687
  if (!compilerOptions.typeRoots) {
1649
1688
  const roots = [path.join(rootDir, "node_modules", "@types")];
1650
1689
  try {
1651
1690
  const req = createRequire(path.join(ENGINE_DIR, "scan.mjs"));
1652
- roots.push(path.dirname(path.dirname(req.resolve("@types/node/package.json"))));
1691
+ const nodeTypes = path.dirname(req.resolve("@types/node/package.json"));
1692
+ const engineTypes = path.dirname(nodeTypes);
1693
+ // ⟨SOUNDNESS R1062⟩ …AND ONLY @types/node. A type ROOT is not just where `types: ["node"]` is looked up: the
1694
+ // compiler also resolves a TARGET's bare import (`import type { Node } from "estree"`) through it when the
1695
+ // target has no package of that name installed. The engine's directory holds whatever else was installed
1696
+ // beside it — in a development tree, eslint's `@types/estree` — so vitest (scanned without node_modules) had
1697
+ // its `estree` imports resolved against the ENGINE's copy, and four rows that are `Unknown` from a clean
1698
+ // `npm install candor-ts` were absent from the same source run out of the repo (MEASURED; the `@types/node`
1699
+ // version, 25.9.2 vs 25.9.9, moved nothing). A verdict must not depend on the engine's neighbours, so when
1700
+ // that directory holds anything but `node`, the root is a private directory holding `node` alone.
1701
+ let others = [];
1702
+ try { others = fs.readdirSync(engineTypes).filter((n) => n !== "node" && !n.startsWith(".")); } catch { others = []; }
1703
+ if (!others.length) roots.push(engineTypes);
1704
+ else {
1705
+ const shim = fs.mkdtempSync(path.join(os.tmpdir(), "candor-ts-types-"));
1706
+ fs.symlinkSync(nodeTypes, path.join(shim, "node"), "junction");
1707
+ process.on("exit", () => { try { fs.rmSync(shim, { recursive: true, force: true }); } catch { /* best effort */ } });
1708
+ roots.push(shim);
1709
+ }
1653
1710
  } catch {}
1654
1711
  compilerOptions.typeRoots = roots;
1655
1712
  }
@@ -2655,7 +2712,16 @@ function declModule(decl) {
2655
2712
  let m = f.match(/@types\/node\/(.+?)\.d\.ts$/);
2656
2713
  if (m) return m[1];
2657
2714
  if (/typescript\/lib\/lib\..*\.d\.ts$/.test(f)) return "<es-lib>";
2658
- m = f.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//);
2715
+ // ⟨SOUNDNESS R778⟩ The INNERMOST `node_modules/<pkg>/` owns the file, not the first. pnpm's real path is
2716
+ // `node_modules/.pnpm/fs-extra@11.2.0/node_modules/fs-extra/…` and an npm nested install is
2717
+ // `node_modules/outer/node_modules/inner/…`; the first-segment match keyed every foreign package of a pnpm
2718
+ // tree as `.pnpm` and `inner` as `outer`. MEASURED with the installer the only variable (fx/r778-{npm,pnpm},
2719
+ // EXECUTED to write the file): npm read `['Fs']` and `deny Fs` exit 1, pnpm read `[]` + `invisible:['.pnpm']`
2720
+ // and exit 0 — a FALSE disclosure (`.pnpm` is not a package) hiding a κ-classified effect. For a flat npm
2721
+ // layout the first and last segments are the same one, so no npm key moves. A file directly under a dot-store
2722
+ // with no inner `node_modules/` (no package segment at all) asks the nearest `package.json` instead.
2723
+ m = [...f.matchAll(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//g)].at(-1) ?? null;
2724
+ if (m && m[1].startsWith(".")) m = (() => { const n = nearestPackageName(f); return n ? [null, n] : m; })();
2659
2725
  if (m) {
2660
2726
  // `@types/X` (DefinitelyTyped) provides types for the RUNTIME package X — map it to X so the curated κ
2661
2727
  // tier (keyed by the runtime name: pg/ws/…) fires. Without this a package typed via @types resolved to
@@ -3461,8 +3527,7 @@ function literalHeadHostUrl(expr) {
3461
3527
  // (an assignment rooted at it, another call's argument, an alias, a return) refuses the capture.
3462
3528
  function determinedUrlObject(expr, call, depth = 0) {
3463
3529
  if (!expr || depth > 1) return null;
3464
- while (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isNonNullExpression(expr)
3465
- || ts.isSatisfiesExpression?.(expr)) expr = expr.expression;
3530
+ expr = unwrapArgExpr(expr); // R780: the one transparent-wrapper set
3466
3531
  if (ts.isNewExpression(expr) && ts.isIdentifier(expr.expression)
3467
3532
  && (expr.expression.text === "URL" || expr.expression.text === "Request")) {
3468
3533
  const decls = checker.getSymbolAtLocation(expr.expression)?.declarations ?? [];
@@ -3516,8 +3581,17 @@ function determinedUrlObject(expr, call, depth = 0) {
3516
3581
  visit(d.getSourceFile());
3517
3582
  return safe ? determinedUrlObject(d.initializer, call, depth + 1) : null;
3518
3583
  }
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.
3584
+ // ⟨SOUNDNESS R780⟩ THE ONE TRANSPARENT-WRAPPER SET. Parentheses, `as`, `<T>x`, `!` and `satisfies` change no runtime
3585
+ // value, so every "is this expression THAT reference / THAT object?" question asks it through this one function.
3586
+ // There were seventeen hand-rolled loops answering it with FIVE different wrapper sets (parens only; +`as`;
3587
+ // +`!`; +`satisfies`; +`<T>`), and every gap was a spelling the engine went blind on: `(k.m as F).call(k)`,
3588
+ // `Reflect.apply(f as F, …)` and `(<any>globalThis).process.env.HOME` were ABSENT (EXECUTED, lane fixture
3589
+ // `fx/r780`), `(f as F).bind(t)` and `run((f))` were opaque. Two loops are deliberately NOT routed here:
3590
+ // `conversionSourceTracked` stops at `as` because an `as` IS the conversion it is asking about (R958 reads the
3591
+ // assertion separately), and `rootsAtStdStream` is a SUPPRESSOR (it frees `process.stdout.write` of a fabricated
3592
+ // Net) — widening a suppressor removes charges, which is not this fix's direction.
3593
+ // (Also R947: the invoked function's own ARGUMENTS on the reflective routes, or `null` when they are not
3594
+ // visible here — a spread, an `.apply` of a non-literal array. `null` is the safe answer: it captures nothing.)
3521
3595
  function unwrapArgExpr(e) {
3522
3596
  while (e && (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isTypeAssertionExpression?.(e)
3523
3597
  || ts.isNonNullExpression(e) || ts.isSatisfiesExpression?.(e))) e = e.expression;
@@ -3701,6 +3775,14 @@ function kappaOfRef(d2, ref) {
3701
3775
  function netEstablishingVerb(member, mod) {
3702
3776
  return NET_ESTABLISHING.has(member) || (/^(node:)?dgram$/.test(mod ?? "") && member === "send");
3703
3777
  }
3778
+ // ⟨R966⟩ see the (CLASSIFY) arm. A package whose `listen` is not a network accept: JSON-RPC connections start
3779
+ // reading a transport chosen (and, when it is a socket, charged) where the transport was built.
3780
+ const NON_NETWORK_LISTEN_PKGS = /^(vscode-jsonrpc|vscode-languageserver(-protocol)?|vscode-languageclient)(\/|$)/;
3781
+ function dependencyListenIsAccept(mod, decl) {
3782
+ if (!mod || mod === "<local>" || mod === "<es-lib>" || mod.startsWith("/") || declIsNodeTypes(decl) || isOwnPackageDecl(decl))
3783
+ return false;
3784
+ return !NON_NETWORK_LISTEN_PKGS.test(mod);
3785
+ }
3704
3786
  function netAcceptingVerb(member, kMod) {
3705
3787
  return NET_ACCEPTING.has(member) || (/^(node:)?inspector(\/promises)?$/.test(kMod ?? "") && member === "open");
3706
3788
  }
@@ -3990,6 +4072,7 @@ const fns = new Map(); // qualified name -> { direct, edges, hosts, ta
3990
4072
  const bodylessDecls = new Map(); // qual -> {node, mod, abstract, owner, member}
3991
4073
  const unlistedSeen = new Map(); // the κ-coverage ledger: unlisted npm package -> call-site count
3992
4074
  const nodeName = new WeakMap(); // declaration node -> qualified name
4075
+ const classExprCtorQual = new Map(); // ⟨R1016⟩ ClassExpression -> its constructor unit (see `mintClassExprCtor`)
3993
4076
  // ⟨scan-boundary, export-alias⟩ `module.exports = { thing: _internalImplName }` / `exports.thing =
3994
4077
  // _internalImplName` — a RENAMED re-export whose RHS NAMES an EXISTING declaration rather than being an
3995
4078
  // inline function. `localName` mints a CJS unit for `module.exports = function(){}` and
@@ -4142,6 +4225,14 @@ const CHA_FANOUT_LIMIT = 12;
4142
4225
  // rather than inlined for the reason `CHA_FANOUT_LIMIT` was: two literals for one rule is how they drift.
4143
4226
  const TYPINGS_CENSUS_CAP = 128;
4144
4227
  const classOverrides = new Map();// base-method MemberDeclaration node -> overriding subclass member nodes (class-CHA)
4228
+ // ⟨SOUNDNESS R873⟩ An override that is not a class MEMBER: `this.m = () => …` assigned inside an instance body of
4229
+ // class `C` replaces `m` on every `C` instance. Registered in `classOverrides` like a member, keyed to the class
4230
+ // that performs the assignment here, because the node's `.parent` is an expression, not the class.
4231
+ const assignedOverrideClass = new Map(); // assigned override node -> the class-like node whose instances it overrides
4232
+ // The class an override node belongs to, for receiver-subtree scoping: a member's own class (declaration OR
4233
+ // expression — ⟨R956⟩), or the assigning class of a ⟨R873⟩ assigned override.
4234
+ const overrideOwner = (om) => assignedOverrideClass.get(om)
4235
+ ?? ((om?.parent && (ts.isClassDeclaration(om.parent) || ts.isClassExpression(om.parent))) ? om.parent : null);
4145
4236
  const classDescendants = new Map();// base ClassDeclaration -> transitive LOCAL subclass ClassDeclarations (coercion-CHA)
4146
4237
  // R954 — the conformer registries (filled by the conversion pass after `walkStructural`).
4147
4238
  const classConformers = new Map(); // local ClassDeclaration -> conformer nodes (local class, literal, dep class)
@@ -4226,7 +4317,7 @@ function overrideClosure(seeds) {
4226
4317
  const out = [];
4227
4318
  for (const s of seeds) {
4228
4319
  for (const m of [s, ...overrideDescent(s)]) {
4229
- if (isOverridableMethod(m) && !out.includes(m)) out.push(m);
4320
+ if ((isOverridableMethod(m) || assignedOverrideClass.has(m)) && !out.includes(m)) out.push(m);
4230
4321
  if (out.length > CHA_FANOUT_LIMIT + 1) return out;
4231
4322
  }
4232
4323
  }
@@ -4277,7 +4368,7 @@ function overrideDescent(decl) {
4277
4368
  function memberDispatchBodies(decl, rootClass) {
4278
4369
  const all = overrideDescent(decl);
4279
4370
  const scoped = (!rootClass || all.length === 0) ? all
4280
- : all.filter((om) => ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass));
4371
+ : all.filter((om) => classInSubtree(overrideOwner(om), rootClass)); // R873/R956: assigned and class-expression overrides
4281
4372
  // R954 — …and the CONFORMERS registered at the receiver's class (a value the checker showed
4282
4373
  // converted to it, or to a subclass of it), which run for this dispatch exactly as an override does. Scoped
4283
4374
  // the same way: registered at `Ct` and its ancestors, so a SUBTYPE receiver never sees them.
@@ -4522,6 +4613,7 @@ const STABLE_KEY_SHAPES = {
4522
4613
  callable: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
4523
4614
  decorator: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
4524
4615
  "decorator-arg": new Set([ts.SyntaxKind.CallExpression]),
4616
+ classexpr: new Set([ts.SyntaxKind.ClassExpression]), // ⟨R1016⟩ a class expression's constructor unit
4525
4617
  computed: null, // a descriptor member of any shape — counted over EVERY node under the anchor
4526
4618
  };
4527
4619
  const stableKeySanitise = (s) => s.replace(/[^A-Za-z0-9_$]/g, "_");
@@ -5093,6 +5185,7 @@ for (const sf of sources) {
5093
5185
  // (a wholly-unresolvable target leaves only the minted unit — nothing to join a forcing site to.)
5094
5186
  }
5095
5187
  }
5188
+ if (ts.isClassExpression(node)) mintClassExprCtor(node); // ⟨R1016⟩
5096
5189
  ts.forEachChild(node, collect);
5097
5190
  })(sf);
5098
5191
  }
@@ -5153,7 +5246,9 @@ for (const { mod, name, ident } of exportAliasCandidates) {
5153
5246
  const baseClassOf = localBaseClassOf;
5154
5247
  for (const sf of sources) {
5155
5248
  (function scan(node) {
5156
- if (ts.isClassDeclaration(node)) {
5249
+ // ⟨SOUNDNESS R956⟩ a CLASS EXPRESSION is a subclass like any other: `new (class extends BaseC { m(){…} })()`
5250
+ // reaching a `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED) because this index read declarations only.
5251
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
5157
5252
  // Local-DESCENDANT index (the coercion-CHA universe, below). classOverrides is keyed by an
5158
5253
  // ANCESTOR MEMBER, so it only sees an override whose base ALSO declares the name. The coercion
5159
5254
  // protocol's members are the opposite shape: `class Sub extends Base { toString(){…} }` where
@@ -5190,6 +5285,7 @@ for (const { mod, name, ident } of exportAliasCandidates) {
5190
5285
  if (!classOverrides.has(ancestor)) classOverrides.set(ancestor, []);
5191
5286
  classOverrides.get(ancestor).push(m);
5192
5287
  foundLocal = true;
5288
+ mintClassExprOverride(node, m, name); // R956
5193
5289
  break;
5194
5290
  }
5195
5291
  base = baseClassOf(base);
@@ -5209,16 +5305,166 @@ for (const { mod, name, ident } of exportAliasCandidates) {
5209
5305
  if (!foreignClassOverrides.has(fm)) foreignClassOverrides.set(fm, []);
5210
5306
  const arr = foreignClassOverrides.get(fm);
5211
5307
  if (!arr.includes(m)) arr.push(m);
5308
+ mintClassExprOverride(node, m, name); // R956
5212
5309
  }
5213
5310
  }
5214
5311
  }
5215
5312
  }
5216
5313
  }
5314
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) registerAssignedOverrides(node); // R873
5217
5315
  ts.forEachChild(node, scan);
5218
5316
  })(sf);
5219
5317
  }
5220
5318
  }
5221
5319
 
5320
+ // ⟨SOUNDNESS R1016⟩ A CLASS EXPRESSION'S CONSTRUCTOR IS A UNIT, as a class declaration's always was. Without one,
5321
+ // `const L = class { constructor() { fs.writeFileSync(…) } }; new L()` and `new Holder.Inner()` over a
5322
+ // `static Inner = class {…}` had nothing to edge to: the constructing function read `['Unknown']` (`deny Fs` exit 0
5323
+ // on a write that ran — EXECUTED), and the body's `Fs` fell through `enclosing` to whatever EVALUATES the class
5324
+ // (`<module>`, `Holder.<static-init>`), which never constructs it (EXECUTED: importing the module writes nothing).
5325
+ // The unit holds what runs at construction — the explicit constructor's body and parameters, and every INSTANCE
5326
+ // field initialiser that is not itself a function value (that one runs when called, and stays where it was) —
5327
+ // so `new` resolves to it through the checker like any constructor, and an implicit constructor is found through
5328
+ // the constructed value's TYPE (`classExprCtorOfNew`). Minted only where there is something to hold.
5329
+ //
5330
+ // WHETHER THE EVALUATING UNIT KEEPS THE CHARGE is ⟨R519⟩'s question, and its answer stands: minting is an
5331
+ // ADDITION unless every construction is provably one this scan edges. A class VALUE that escapes — exported,
5332
+ // returned, passed, stored, `extends`-ed, read as a static — can be constructed where no edge reaches it (another
5333
+ // package, which cannot name an anonymous class's constructor; an `any`-typed `new`), so pass 2a′ keeps the
5334
+ // evaluator's containment edge for it. Only a `const`-bound, unexported class expression whose every reference
5335
+ // is a `new` callee (or a type position, or an `instanceof` operand) drops it — there, every run of the body is
5336
+ // a `new` the scan resolved, and the evaluator's charge was the fabrication R1016 measured.
5337
+ function mintClassExprCtor(cls) {
5338
+ const ctor = (cls.members ?? []).find((m) => ts.isConstructorDeclaration(m) && m.body);
5339
+ const fieldInit = (cls.members ?? []).some((m) => ts.isPropertyDeclaration(m) && m.initializer
5340
+ && !(ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)
5341
+ && !ts.isArrowFunction(m.initializer) && !ts.isFunctionExpression(m.initializer));
5342
+ if (!ctor && !fieldInit) return null;
5343
+ const sf = cls.getSourceFile();
5344
+ const local = `<class>@${stableUnitTag("classexpr", cls)}.constructor`;
5345
+ const qual = `${moduleOf(sf)}.${local}`;
5346
+ if (!fns.has(qual)) {
5347
+ const at = ctor ?? cls;
5348
+ const { line, character } = sf.getLineAndCharacterOfPosition(at.getStart());
5349
+ fns.set(qual, { local, direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
5350
+ cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(),
5351
+ why: new Set(), entry: false,
5352
+ loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}`,
5353
+ endLine: sf.getLineAndCharacterOfPosition(at.getEnd()).line + 1 });
5354
+ }
5355
+ if (ctor) nodeName.set(ctor, qual);
5356
+ classExprCtorQual.set(cls, qual);
5357
+ if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH mint ${qual}`);
5358
+ return qual;
5359
+ }
5360
+ // The class-expression constructor unit a `new X(…)` runs, read off the constructed value's TYPE — the route an
5361
+ // IMPLICIT constructor needs (the signature then has no declaration, or names the BASE's), and harmless beside an
5362
+ // explicit one, which the signature already names.
5363
+ function classExprCtorOfNew(node) {
5364
+ if (!classExprCtorQual.size || !ts.isNewExpression(node) || !node.expression) return null;
5365
+ let vd;
5366
+ try { vd = checker.getTypeAtLocation(node.expression)?.getSymbol?.()?.valueDeclaration; } catch { vd = undefined; }
5367
+ return vd && ts.isClassExpression(vd) ? (classExprCtorQual.get(vd) ?? null) : null;
5368
+ }
5369
+ // Does this class expression's VALUE reach anything but a `new` this scan resolves? (see `mintClassExprCtor`)
5370
+ function classExprEscapes(cls) {
5371
+ let top = cls;
5372
+ while (top.parent && (ts.isParenthesizedExpression(top.parent) || ts.isAsExpression(top.parent)
5373
+ || ts.isNonNullExpression(top.parent) || (ts.isSatisfiesExpression?.(top.parent) ?? false))) top = top.parent;
5374
+ const vd = top.parent;
5375
+ if (!vd || !ts.isVariableDeclaration(vd) || vd.initializer !== top || !ts.isIdentifier(vd.name)
5376
+ || !(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const)
5377
+ || (ts.getCombinedModifierFlags(vd) & ts.ModifierFlags.Export)) return true;
5378
+ const syms = new Set();
5379
+ const outer = checker.getSymbolAtLocation(vd.name);
5380
+ if (!outer) return true;
5381
+ syms.add(outer);
5382
+ if (cls.name) { const inner = checker.getSymbolAtLocation(cls.name); if (inner) syms.add(inner); }
5383
+ const names = new Set([vd.name.text, cls.name?.text].filter(Boolean));
5384
+ let escapes = false;
5385
+ (function visit(n) {
5386
+ if (escapes) return;
5387
+ if (ts.isIdentifier(n) && names.has(n.text) && n !== vd.name && n !== cls.name) {
5388
+ let sym; try { sym = checker.getSymbolAtLocation(n); } catch { sym = undefined; }
5389
+ if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
5390
+ const p0 = n.parent;
5391
+ if (p0 && ts.isShorthandPropertyAssignment(p0) && p0.name === n) { escapes = true; return; }
5392
+ if (sym && syms.has(sym)) {
5393
+ let cur = n;
5394
+ while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
5395
+ || ts.isAsExpression(cur.parent) || (ts.isSatisfiesExpression?.(cur.parent) ?? false))) cur = cur.parent;
5396
+ const p = cur.parent;
5397
+ const safe = !!p && ((ts.isNewExpression(p) && p.expression === cur)
5398
+ || ts.isTypeQueryNode(p)
5399
+ || (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword));
5400
+ if (!safe) { escapes = true; return; }
5401
+ }
5402
+ }
5403
+ ts.forEachChild(n, visit);
5404
+ })(vd.getSourceFile());
5405
+ return escapes;
5406
+ }
5407
+ // ⟨SOUNDNESS R956⟩ A class EXPRESSION's member has no unit (`localName` names class DECLARATION members only), so
5408
+ // its body folded into whatever evaluates the expression and no dispatch could land on it. An override needs an
5409
+ // addressable body: mint it the way a structural implementor's member is minted — keyed by `stableUnitTag`, with
5410
+ // ⟨R519⟩'s containment edge so the evaluating unit keeps every charge it had (an ADDITION, never a move). Only a
5411
+ // member that OVERRIDES is minted; a class expression's other members stay where they were.
5412
+ function mintClassExprOverride(cls, m, name) {
5413
+ if (!ts.isClassExpression(cls) || nodeName.has(m)) return;
5414
+ const bodied = (ts.isMethodDeclaration(m) && m.body)
5415
+ || (ts.isPropertyDeclaration(m) && m.initializer && (ts.isArrowFunction(m.initializer) || ts.isFunctionExpression(m.initializer)));
5416
+ if (!bodied) return; // an unminted override reads unresolved at every reader — disclosed, never silent
5417
+ const sf = m.getSourceFile();
5418
+ mintPositionalStructuralUnit(moduleOf(sf), sf, m, name);
5419
+ if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH classexpr ${name}`);
5420
+ }
5421
+ // ⟨SOUNDNESS R873⟩ `this.m = <value>` inside an INSTANCE body of class `C` (its constructor, a method, an accessor,
5422
+ // an arrow property — not a nested `function`, which rebinds `this`) replaces `m` on that instance. Where `C` or a
5423
+ // local ancestor declares METHOD `m`, the assignment is an override of it: a `Base`-typed `b.m()` on a `C` runs the
5424
+ // assigned value. MEASURED: `class SubC extends BaseC { constructor(){ super(); this.m = () => write } }` with a
5425
+ // `BaseC`-typed `b.m()` was ABSENT at every gate (EXECUTED), the arrow charged to `SubC.constructor` only.
5426
+ // · a function literal is minted (`mintCallTargetUnit`, containment edge kept: the assigning body still carries it)
5427
+ // and registered — a resolution;
5428
+ // · `this.m = this.m.bind(…)` / `this.m = this.m` re-installs the same method and registers nothing;
5429
+ // · any other value (a wrapper's result, another method's `.bind`, a parameter) is registered UNRESOLVED, so every
5430
+ // reader's `allResolved` gate discloses `dispatch:` rather than certifying the declared body alone.
5431
+ // A PROPERTY `m` is not this rule's: a written property slot is ⟨R103⟩'s, which already discloses.
5432
+ function registerAssignedOverrides(cls) {
5433
+ const memberNamed = (c, name) => (c.members ?? []).find((x) => ts.isMethodDeclaration(x) && x.name?.getText?.() === name);
5434
+ for (const mem of cls.members ?? []) {
5435
+ if (ts.getCombinedModifierFlags(mem) & ts.ModifierFlags.Static) continue;
5436
+ const body = (ts.isConstructorDeclaration(mem) || ts.isMethodDeclaration(mem) || ts.isGetAccessorDeclaration(mem)
5437
+ || ts.isSetAccessorDeclaration(mem)) ? mem.body
5438
+ : (ts.isPropertyDeclaration(mem) && mem.initializer && ts.isArrowFunction(mem.initializer)) ? mem.initializer.body : null;
5439
+ if (!body) continue;
5440
+ (function walk(n) {
5441
+ if (n !== body && (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isClassLike(n))) return;
5442
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken
5443
+ && ts.isPropertyAccessExpression(n.left) && n.left.expression.kind === ts.SyntaxKind.ThisKeyword) {
5444
+ const name = n.left.name.getText();
5445
+ let anc = memberNamed(cls, name);
5446
+ for (let b = localBaseClassOf(cls), g = 0; !anc && b && g++ < 64; b = localBaseClassOf(b)) anc = memberNamed(b, name);
5447
+ if (anc) {
5448
+ const rhs = unwrapArgExpr(n.right);
5449
+ const bound = rhs && ts.isCallExpression(rhs) ? unwrapBind(rhs) : null;
5450
+ const ref = bound ? bound.ref : rhs;
5451
+ const same = ref && ts.isPropertyAccessExpression(ref) && ref.expression.kind === ts.SyntaxKind.ThisKeyword
5452
+ && ref.name.getText() === name;
5453
+ if (!same) {
5454
+ const node = rhs && (ts.isArrowFunction(rhs) || ts.isFunctionExpression(rhs)) ? rhs : n;
5455
+ if (node === rhs && !callTargetUnit(rhs)) mintCallTargetUnit(rhs);
5456
+ if (!classOverrides.has(anc)) classOverrides.set(anc, []);
5457
+ const arr = classOverrides.get(anc);
5458
+ if (!arr.includes(node)) { arr.push(node); assignedOverrideClass.set(node, cls); }
5459
+ if (process.env.CANDOR_R873_REACH) console.error(`R873-REACH assigned ${node === rhs ? "fn" : "opaque"} ${name}`);
5460
+ }
5461
+ }
5462
+ }
5463
+ ts.forEachChild(n, walk);
5464
+ })(body);
5465
+ }
5466
+ }
5467
+
5222
5468
  // callback-flow bookkeeping (the Rust engine's callback_named move, ported): for every call that
5223
5469
  // edges to a LOCAL unit, record what each argument position received — a NAMED local unit (a
5224
5470
  // resolvable callback target), or an opaque value (an inline closure stays attributed to the
@@ -5233,7 +5479,8 @@ const callbackArgs = new Map(); // calleeName -> Map(argIndex -> Array<{calle
5233
5479
  // What each argument position of a call to LOCAL unit `targetName` received (callback flow, see `callbackArgs`).
5234
5480
  // Shared by the ordinary local arm and R955's callee-choice arm, so a callback passed through either is seen.
5235
5481
  function registerCallbackArgs(rec, targetName, args) {
5236
- args.forEach((a, i) => {
5482
+ args.forEach((a0, i) => {
5483
+ const a = unwrapArgExpr(a0); // R780: `run((f))` / `run(f as F)` pass the same function as `run(f)`
5237
5484
  const slot = (callbackArgs.get(targetName) ?? callbackArgs.set(targetName, new Map()).get(targetName));
5238
5485
  const list = slot.get(i) ?? [];
5239
5486
  let target = null, opaque = false;
@@ -5372,8 +5619,7 @@ if (R103_HITS) process.on("exit", () => {
5372
5619
  });
5373
5620
  function openCallSlot(node) {
5374
5621
  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;
5622
+ const callee = unwrapArgExpr(node.expression); // R780
5377
5623
  // ELEMENT ACCESS IS THE SAME SLOT BY ANOTHER SPELLING, and it is here because the first draft of this
5378
5624
  // comment asserted the opposite — "an element access already reaches the dynamic-key/`callback:` arms"
5379
5625
  // — and that was FALSE when measured: `this["handler"]()` and `this[k]()` (k narrowed to the literal
@@ -5439,8 +5685,8 @@ function unwrapBind(node, depth = 0) {
5439
5685
  if (!node || depth > 8) return null;
5440
5686
  if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
5441
5687
  if (node.expression.name.text !== "bind") return null;
5442
- let recv = node.expression.expression;
5443
- while (ts.isParenthesizedExpression(recv)) recv = recv.expression;
5688
+ // R780: `(f as F).bind(t)` / `(f satisfies F).bind(t)` bind the same function — the ONE wrapper set, not parens only.
5689
+ const recv = unwrapArgExpr(node.expression.expression);
5444
5690
  // chained `.bind().bind()` — recurse only when the receiver is ITSELF a `.bind` call, else it's an
5445
5691
  // arbitrary call (`getCallback().bind`) whose result we can't pin → unresolvable bind.
5446
5692
  if (ts.isCallExpression(recv)) {
@@ -6012,12 +6258,13 @@ function recordConformer(c, t) {
6012
6258
  }
6013
6259
  if (ts.isClassDeclaration(t)) {
6014
6260
  if (c.kind === "class" && classInSubtree(c.node, t)) return false; // nominal: the subtree already answers
6015
- // R958 — an ASSERTED downcast (`base as Sub`, the target inside the conformer's own subtree) names no new
6016
- // conformer: the receiver's class arm answers `Sub`'s subtree. Scoped to an ASSERTION on purpose. Without one,
6017
- // `f(new Base())` into `f(s: Sub)` passed the checker's own (structural) check and the value really IS a
6018
- // `Base` — refusing that would drop the body that runs. MEASURED: the guard unscoped is load-bearing (a
6019
- // structurally identical `Base` IS assignable to `Sub`), so its scope decides which of the two it decides.
6020
- if (c.kind === "class" && conversionViaAssertion && classInSubtree(t, c.node)) return false;
6261
+ // ⟨SOUNDNESS R958, the DOWNCAST half⟩ An asserted downcast (`base as Sub`) IS a new conformer when the value is a
6262
+ // `Base`, and nothing here can tell that apart from a truthful one. A refusal stood here, scoped to assertions,
6263
+ // on the claim that "the receiver's class arm answers `Sub`'s subtree" — but the value's class is `Base`, which
6264
+ // is NOT in `Sub`'s subtree, so `Base.m` was the body that ran and the one no arm named. EXECUTED: `dO(bO as
6265
+ // SubO)` wrote a file through `BaseO.m` with `dO`/`rO` ABSENT and `pure a.dO` exit 0, while `dP(bP)` — the same
6266
+ // value without the `as` — was `Fs`, exit 1. An assertion must not delete a charge the unasserted conversion
6267
+ // makes. On a TRUTHFUL downcast the registration over-charges a `Sub`-typed receiver with `Base`'s override.
6021
6268
  let added = false;
6022
6269
  for (const anc of localAncestorsAndSelf(t)) added = pushUnique(classConformers, anc, c.node) || added;
6023
6270
  if (c.kind === "lit") mintStructuralMembers(c.node, true);
@@ -6090,12 +6337,7 @@ function isConversionPosition(n) {
6090
6337
  return false;
6091
6338
  }
6092
6339
  // R958 — the value under an assertion chain: parentheses, `as`, `<T>`, `!` and `satisfies` change no runtime value.
6093
- 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
- }
6340
+ const peelAssertions = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a third copy of it
6099
6341
  // R82 / PART 87: is the conversion's SOURCE type a fact about the VALUE, or only about a generic signature? A
6100
6342
  // call whose declared return type mentions one of its own type parameters (`wrap<T>(x: T): T`) hands back
6101
6343
  // whatever type the ARGUMENT had, and that signature proves assignability, never that the value IS the
@@ -6129,20 +6371,182 @@ function conversionSourceTracked(e, depth = 0) {
6129
6371
  }
6130
6372
  return true;
6131
6373
  }
6374
+ // ---- PARAMETER PROVENANCE: what every VISIBLE call site binds to a parameter (SOUNDNESS R934, R246) ----
6375
+ // A parameter's value is whatever its callers pass. For a function whose every reference is a CALL — not
6376
+ // handed out as a value, not `.call`/`.apply`/`.bind`-ed — the visible call sites ARE its bindings, and the
6377
+ // arguments there say what the parameter can hold. Two consumers ask this ONE index rather than each
6378
+ // walking calls: the environment-location model (a parameter every site hands `process.env`, R934) and
6379
+ // the `any`-receiver accessor arm (an `any`-typed parameter whose every site passes a typed value, R246).
6380
+ // `paramArgs` maps a parameter key (its symbol; a destructured parameter's NODE) to the argument expression
6381
+ // at each visible site (`null` where a spread hides the position; the default, or `null` for `undefined`,
6382
+ // where the site omits it). `paramFnEscaped(fnSym)` answers whether the function escapes, batched.
6383
+ const paramArgs = new Map();
6384
+ const paramOwnerOf = new Map(); // param key -> function symbol (for the escape question)
6385
+ const paramDeclOf = new Map(); // param key -> its ParameterDeclaration
6386
+ let paramFnEscaped;
6387
+ // A class some project class `extends` — its members are excluded (an override is invisible here).
6388
+ const extendedClassSyms = new Set();
6389
+ const classSymOf = (cls) => (cls?.name ? checker.getSymbolAtLocation(cls.name) : cls?.symbol) ?? null;
6390
+ {
6391
+ const noteArg = (k, v) => { if (k) (paramArgs.get(k) ?? paramArgs.set(k, []).get(k)).push(v); };
6392
+ const collectHeritage = (node) => {
6393
+ if ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) && node.heritageClauses) {
6394
+ for (const h of node.heritageClauses) if (h.token === ts.SyntaxKind.ExtendsKeyword) {
6395
+ for (const t of h.types) {
6396
+ let s = checker.getSymbolAtLocation(t.expression);
6397
+ if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
6398
+ if (s) extendedClassSyms.add(s);
6399
+ }
6400
+ }
6401
+ }
6402
+ ts.forEachChild(node, collectHeritage);
6403
+ };
6404
+ for (const sf of sources) collectHeritage(sf);
6405
+ const calls = [];
6406
+ const collectCalls = (node) => {
6407
+ if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) > 0) calls.push(node);
6408
+ ts.forEachChild(node, collectCalls);
6409
+ };
6410
+ for (const sf of sources) collectCalls(sf);
6411
+ // ⟨R934⟩ the functions whose parameters are eligible locations: a function declaration, a function or
6412
+ // arrow bound to a `const`, a constructor, or a method of a class with no heritage that nothing extends.
6413
+ // Every reference to it must be a CALL (or an export, a `typeof`, a member read that is not
6414
+ // `.call`/`.apply`/`.bind`) — anything else hands it to code that binds its parameters unseen.
6415
+ const fnEligible = new Map(); // declaration -> boolean
6416
+ const fnSymOf = (decl) => {
6417
+ if (ts.isFunctionDeclaration(decl) || ts.isMethodDeclaration(decl)) return decl.name ? checker.getSymbolAtLocation(decl.name) : null;
6418
+ if ((ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) && ts.isVariableDeclaration(decl.parent)
6419
+ && decl.parent.initializer === decl && ts.isIdentifier(decl.parent.name)
6420
+ && (ts.getCombinedNodeFlags(decl.parent) & ts.NodeFlags.Const)) return checker.getSymbolAtLocation(decl.parent.name);
6421
+ if (ts.isConstructorDeclaration(decl)) return classSymOf(decl.parent);
6422
+ return null;
6423
+ };
6424
+ const declShapeOk = (decl) => {
6425
+ if (!decl?.body || !projectFiles.has(path.resolve(decl.getSourceFile().fileName))) return false;
6426
+ if (ts.isFunctionDeclaration(decl) || ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) return true;
6427
+ if (ts.isConstructorDeclaration(decl)) return true;
6428
+ if (ts.isMethodDeclaration(decl) && (ts.isClassDeclaration(decl.parent) || ts.isClassExpression(decl.parent)))
6429
+ return !decl.parent.heritageClauses?.length && !extendedClassSyms.has(classSymOf(decl.parent));
6430
+ return false;
6431
+ };
6432
+ const escapedFnSyms = new Set();
6433
+ const escapeScannedSyms = new Set();
6434
+ // One traversal per BATCH of function symbols not yet asked about; the answer is cached.
6435
+ const scanEscapes = (cands0) => {
6436
+ const cands = new Set([...cands0].filter((x) => x && !escapeScannedSyms.has(x)));
6437
+ if (!cands.size) return;
6438
+ for (const c of cands) escapeScannedSyms.add(c);
6439
+ const names = new Set([...cands].map((s) => s.name));
6440
+ const SAFE_MEMBER_BLOCK = new Set(["call", "apply", "bind"]);
6441
+ const visit = (node) => {
6442
+ if (ts.isIdentifier(node) && names.has(node.text)) {
6443
+ let s = checker.getSymbolAtLocation(node);
6444
+ if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { /* keep */ } }
6445
+ if (s && cands.has(s) && !escapedFnSyms.has(s)) {
6446
+ // climb the transparent wrappers to the position the value lands in
6447
+ let cur = node;
6448
+ const p0 = node.parent;
6449
+ let safe = false;
6450
+ if (p0 && (ts.isPropertyAccessExpression(p0) || ts.isPropertyAssignment(p0) || ts.isMethodDeclaration(p0)
6451
+ || ts.isFunctionDeclaration(p0) || ts.isClassDeclaration(p0) || ts.isClassExpression(p0)
6452
+ || ts.isVariableDeclaration(p0) || ts.isPropertyDeclaration(p0)) && p0.name === node) {
6453
+ // a declaration name, or `o.name` — a METHOD reference: safe only as the callee of a call
6454
+ if (ts.isPropertyAccessExpression(p0)) {
6455
+ const pp = p0.parent;
6456
+ safe = !!pp && (ts.isCallExpression(pp) || ts.isNewExpression(pp)) && pp.expression === p0;
6457
+ } else safe = true;
6458
+ } else {
6459
+ while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent))) cur = cur.parent;
6460
+ const p = cur.parent;
6461
+ if (!p) safe = true;
6462
+ else if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === cur) safe = true;
6463
+ else if (ts.isPropertyAccessExpression(p) && p.expression === cur) safe = !SAFE_MEMBER_BLOCK.has(p.name.text);
6464
+ else if (ts.isExportSpecifier(p) || ts.isExportAssignment(p) || ts.isTypeQueryNode(p)
6465
+ || ts.isTypeOfExpression(p) || ts.isExpressionWithTypeArguments(p) || ts.isImportSpecifier(p)
6466
+ || ts.isImportClause(p) || ts.isNamespaceImport(p) || ts.isTypeReferenceNode(p)
6467
+ || ts.isQualifiedName(p)) safe = true;
6468
+ else if (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) safe = true;
6469
+ }
6470
+ if (!safe) escapedFnSyms.add(s);
6471
+ }
6472
+ }
6473
+ ts.forEachChild(node, visit);
6474
+ };
6475
+ for (const sf of sources) visit(sf);
6476
+ };
6477
+ // Seed the parameter bindings: for every call whose resolved declaration is an eligible shape, bind each
6478
+ // identifier parameter to its argument (or default / undefined). Only calls that hand over at least one
6479
+ // argument are recorded here — a site that passes nothing binds `undefined` to every parameter, which
6480
+ // is recorded by the per-declaration site count below.
6481
+ const sitesByDecl = new Map(); // decl -> number of call sites seen
6482
+ for (const call of calls) {
6483
+ let decl;
6484
+ try { decl = checker.getResolvedSignature(call)?.declaration; } catch { decl = null; }
6485
+ if (!decl || !declShapeOk(decl)) continue;
6486
+ if (!fnEligible.has(decl)) fnEligible.set(decl, !!fnSymOf(decl));
6487
+ if (!fnEligible.get(decl)) continue;
6488
+ sitesByDecl.set(decl, (sitesByDecl.get(decl) ?? 0) + 1);
6489
+ const fsym = fnSymOf(decl);
6490
+ let pos = 0, sawSpread = false;
6491
+ const args = call.arguments ?? [];
6492
+ const argAt = [];
6493
+ for (const a of args) { if (ts.isSpreadElement(a)) { sawSpread = true; break; } argAt[pos++] = a; }
6494
+ decl.parameters.forEach((p, i) => {
6495
+ if (p.dotDotDotToken) return;
6496
+ // a DESTRUCTURED parameter reads its keys when the frame is entered — keyed by the parameter node
6497
+ const ps = ts.isIdentifier(p.name) ? checker.getSymbolAtLocation(p.name)
6498
+ : ts.isObjectBindingPattern(p.name) ? p : null;
6499
+ if (!ps) return;
6500
+ paramOwnerOf.set(ps, fsym);
6501
+ if (i < argAt.length && argAt[i]) noteArg(ps, argAt[i]);
6502
+ else if (sawSpread) noteArg(ps, null);
6503
+ else noteArg(ps, p.initializer ?? null);
6504
+ });
6505
+ }
6506
+ // A call site that resolves to the declaration with NO arguments was not collected above; it binds
6507
+ // every parameter to its default or `undefined`. Count them so a zero-argument site is not lost.
6508
+ {
6509
+ const visitZero = (node) => {
6510
+ if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && (node.arguments?.length ?? 0) === 0) {
6511
+ let decl;
6512
+ try { decl = checker.getResolvedSignature(node)?.declaration; } catch { decl = null; }
6513
+ if (decl && sitesByDecl.has(decl)) for (const p of decl.parameters)
6514
+ if (!p.dotDotDotToken && ts.isIdentifier(p.name)) noteArg(checker.getSymbolAtLocation(p.name), p.initializer ?? null);
6515
+ else if (!p.dotDotDotToken && ts.isObjectBindingPattern(p.name)) noteArg(p, p.initializer ?? null);
6516
+ }
6517
+ ts.forEachChild(node, visitZero);
6518
+ };
6519
+ if (sitesByDecl.size) for (const sf of sources) visitZero(sf);
6520
+ }
6521
+ for (const k of paramArgs.keys()) { const d = ts.isParameter(k) ? k : k.valueDeclaration; if (d && ts.isParameter(d)) paramDeclOf.set(k, d); }
6522
+ paramFnEscaped = (fsym) => { if (!fsym) return true; scanEscapes([fsym]); return escapedFnSyms.has(fsym); };
6523
+ // Batch form: answer for many at once with ONE traversal.
6524
+ paramFnEscaped.batch = (fsyms) => scanEscapes(fsyms);
6525
+ }
6526
+
6132
6527
  const CONFORMER_REACH = process.env.CANDOR_CONFORMER_REACH ? (k, n) => console.error(`CONFORMER-REACH ${k} ${n}`) : null;
6133
6528
  for (const sf of sources) {
6134
6529
  const convert = (src, tt, at, kind) => {
6135
6530
  if (!tt || !conversionSourceTracked(src)) return;
6136
6531
  let st; try { st = checker.getTypeAtLocation(src); } catch { st = undefined; }
6137
- conversionViaAssertion = kind !== "conversion";
6138
- try {
6139
- if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
6140
- const count = () => [...interfaceImpls.values(), ...foreignInterfaceImpls.values(), ...classConformers.values(),
6141
- ...depConformers.values()].reduce((a, x) => a + x.length, 0);
6142
- const before = count();
6143
- recordConversion(st, tt);
6144
- if (count() > before) CONFORMER_REACH(kind, `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
6145
- } finally { conversionViaAssertion = false; }
6532
+ // ⟨SOUNDNESS R958, the `any`-PARAMETER half⟩ a source typed `any`/`unknown` names no conformer, so a value
6533
+ // laundered through an `any` parameter reached a closed dispatch silently: `launder(x: any) { qDisp(x) }`
6534
+ // called as `launder(new LocalW())` ran LocalW's `m` (EXECUTED) with `deny Fs` exit 0 on the caller. The
6535
+ // parameter's visible arguments say what it holds — the SAME provenance index R246's accessor arm asks
6536
+ // (`receiverValueTypes`) — so each argument type is recorded as a conversion source. Upcasts only, as
6537
+ // ever (`recordConversion` keeps a target the type is assignable to). ⟨R958 widened-local half⟩ and the
6538
+ // same for a source typed `object`/`{}`, and a `const` declared wider than its initializer
6539
+ // (`uninformativeType`). A dependency-sourced value is R958's disclosed residue (`invisible`).
6540
+ if (uninformativeType(st)) for (const vt of receiverValueTypes(src)) {
6541
+ try { recordConversion(vt, tt); } catch { /* best effort, as the main record below */ }
6542
+ CONFORMER_REACH?.("any-param", `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
6543
+ }
6544
+ if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
6545
+ const count = () => [...interfaceImpls.values(), ...foreignInterfaceImpls.values(), ...classConformers.values(),
6546
+ ...depConformers.values()].reduce((a, x) => a + x.length, 0);
6547
+ const before = count();
6548
+ recordConversion(st, tt);
6549
+ if (count() > before) CONFORMER_REACH(kind, `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
6146
6550
  };
6147
6551
  (function walkConversions(node) {
6148
6552
  if (ts.isExpression(node) && !ts.isSpreadElement(node) && isConversionPosition(node)) {
@@ -6162,9 +6566,53 @@ for (const sf of sources) {
6162
6566
  let tt; try { tt = checker.getTypeFromTypeNode(node.type); } catch { tt = undefined; }
6163
6567
  convert(peelAssertions(node.expression), tt, node, "assertion");
6164
6568
  }
6569
+ if (ts.isObjectLiteralExpression(node)) providerSubstitution(node); // ⟨R1061⟩
6165
6570
  ts.forEachChild(node, walkConversions);
6166
6571
  })(sf);
6167
6572
  }
6573
+ // ⟨SOUNDNESS R1061⟩ A DEPENDENCY-INJECTION PROVIDER IS A CONVERSION THE CHECKER NEVER SEES. NestJS (and Angular,
6574
+ // which shares the shape) wires `{ provide: Store, useClass: FileStore }` — or `useValue: <obj>`, `useFactory: () =>
6575
+ // <obj>`, `useExisting: Other` — and the container then hands the substitute to every `store: Store` parameter. No
6576
+ // expression converts the substitute to `Store`, so a `this.store.save()` resolved to `Store.save` alone: EXECUTED on
6577
+ // NestJS 10, `FileStore.save` wrote a file while the calling method was ABSENT and `deny Fs`/`deny Fs Unknown` exited 0
6578
+ // for all three provider forms. The provider entry IS the conversion, so the substitute is registered as a conformer
6579
+ // of the token class — the union of the declared class and the visible substitute, through the R874/R954 conformer
6580
+ // arm every class dispatch already asks. Registered directly, without the checker's assignability test, because the
6581
+ // container does not apply one (a token class with a private member is not structurally assignable from its
6582
+ // substitute, and is still what gets injected). Only a token that names a LOCAL class registers; a string or symbol
6583
+ // token types its parameter by an interface or `any`, which the interface arm already answers or discloses.
6584
+ function providerSubstitution(lit) {
6585
+ const prop = (name) => lit.properties.find((p) => ts.isPropertyAssignment(p) && p.name && !ts.isComputedPropertyName(p.name)
6586
+ && (p.name.text ?? p.name.getText()) === name);
6587
+ const provide = prop("provide");
6588
+ if (!provide) return;
6589
+ let tsym; try { tsym = checker.getSymbolAtLocation(peelAssertions(provide.initializer)); } catch { tsym = undefined; }
6590
+ if (tsym && (tsym.flags & ts.SymbolFlags.Alias)) { try { tsym = checker.getAliasedSymbol(tsym); } catch { return; } }
6591
+ const target = (tsym?.declarations ?? []).find((d) => ts.isClassDeclaration(d) && isProjectNode(d));
6592
+ if (!target) return;
6593
+ const instanceOf = (e) => {
6594
+ let s; try { s = checker.getSymbolAtLocation(peelAssertions(e)); } catch { s = undefined; }
6595
+ if (s && (s.flags & ts.SymbolFlags.Alias)) { try { s = checker.getAliasedSymbol(s); } catch { s = undefined; } }
6596
+ if (!s || !(s.flags & ts.SymbolFlags.Class)) return null;
6597
+ try { return checker.getDeclaredTypeOfSymbol(s); } catch { return null; }
6598
+ };
6599
+ const sources = [];
6600
+ for (const key of ["useClass", "useExisting"]) { const p = prop(key); const t = p && instanceOf(p.initializer); if (t) sources.push(t); }
6601
+ const uv = prop("useValue");
6602
+ if (uv) { try { sources.push(checker.getTypeAtLocation(peelAssertions(uv.initializer))); } catch { /* none */ } }
6603
+ const uf = prop("useFactory");
6604
+ if (uf) {
6605
+ let ft; try { ft = checker.getTypeAtLocation(uf.initializer); } catch { ft = undefined; }
6606
+ for (const sig of ft?.getCallSignatures?.() ?? []) {
6607
+ let rt; try { rt = sig.getReturnType(); rt = checker.getAwaitedType?.(rt) ?? rt; } catch { rt = undefined; }
6608
+ if (rt) sources.push(rt);
6609
+ }
6610
+ }
6611
+ for (const st of sources) for (const s of st?.isUnion?.() ? st.types : [st]) {
6612
+ const c = conformerOfSourceType(s);
6613
+ if (c && recordConformer(c, target)) CONFORMER_REACH?.("di-provider", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
6614
+ }
6615
+ }
6168
6616
  // The interface-flow fixpoint: a `J`-typed value converted to `X` carries every conformer of `J` with it.
6169
6617
  for (let changed = true, g = 0; changed && g++ < 32; ) {
6170
6618
  changed = false;
@@ -6308,6 +6756,68 @@ function keyLiteralNames(t, depth = 0, seen = new Set()) {
6308
6756
  if (c && c !== t) return keyLiteralNames(c, depth + 1, seen);
6309
6757
  return null; // `string`, `symbol`, `keyof T` for generic T, …
6310
6758
  }
6759
+ // SOUNDNESS R246 — THE RECEIVER'S VALUE, WHEN ITS STATIC TYPE SAYS NOTHING. Accessor lookup asks the
6760
+ // receiver's TYPE which properties it declares, and `any` declares none: `(s as any)[k] = v`,
6761
+ // `(s as any).token = v` and `function w(o: any) { o.token = v }` called as `w(new S())` all invoked S's
6762
+ // setter (EXECUTED, 6 of 6 writes observed) and were ABSENT with `deny Fs <fn>` exit 0 — the caller too.
6763
+ // The value is not unknowable; only its static type was thrown away. Two places say what it really is,
6764
+ // and both are facts rather than guesses:
6765
+ // - an ASSERTION changes no runtime value (R958's look-through, the same `peelAssertions`): the operand
6766
+ // under `as any` / `as unknown as T` / `<any>x` has the type the value really carries;
6767
+ // - an `any`/`unknown` PARAMETER holds what its visible call sites pass (the shared PARAMETER PROVENANCE
6768
+ // index, R934) — only for a function whose every reference is a call; an escaped one is unanswered.
6769
+ // Returns the EXTRA types beyond the checker's own (empty when the checker's type is informative and no
6770
+ // assertion intervenes), so a typed receiver's lookup is byte-identical to before. This RESOLVES — it adds
6771
+ // the accessor edges the value's type declares — and never hedges; a receiver neither rule answers is
6772
+ // exactly as it was (the residual is stated in SOUNDNESS R246, not hidden here).
6773
+ function anyishType(t) { return !!t && !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)); } // hoisted: the conversion walk asks it first
6774
+ // ⟨SOUNDNESS R958, the WIDENED-LOCAL half⟩ A type that names no member is as silent about the value as `any`: a local
6775
+ // declared `object` or `{}` holding `new LocalW()` and asserted into an interface (`qDisp(o as Sink)`) ran
6776
+ // `LocalW.m` (EXECUTED) with every unit `[]` and no disclosure — `anyishType` alone let it through. Asked by the
6777
+ // conversion walk and the R246 receiver question alike; it only ever lets MORE value types be recorded.
6778
+ function uninformativeType(t) {
6779
+ if (!t) return false;
6780
+ if (anyishType(t) || (t.flags & ts.TypeFlags.NonPrimitive)) return true;
6781
+ if (!(t.flags & ts.TypeFlags.Object) || t.isUnion?.() || t.isIntersection?.()) return false;
6782
+ try {
6783
+ return checker.getPropertiesOfType(t).length === 0 && t.getCallSignatures().length === 0
6784
+ && t.getConstructSignatures().length === 0 && !checker.getIndexInfosOfType?.(t)?.length;
6785
+ } catch { return false; }
6786
+ }
6787
+ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
6788
+ if (!expr || depth > 6) return [];
6789
+ let t; try { t = checker.getTypeAtLocation(expr); } catch { t = null; }
6790
+ const inner = peelAssertions(expr);
6791
+ if (inner && inner !== expr) {
6792
+ const below = receiverValueTypes(inner, depth + 1, seen);
6793
+ let ti; try { ti = checker.getTypeAtLocation(inner); } catch { ti = null; }
6794
+ return [...(ti && !anyishType(ti) ? [ti] : []), ...below];
6795
+ }
6796
+ if (!uninformativeType(t) || !ts.isIdentifier(expr)) return [];
6797
+ const sym = checker.getSymbolAtLocation(expr);
6798
+ const d = sym?.valueDeclaration;
6799
+ if (!sym || !d || seen.has(sym)) return [];
6800
+ // ⟨R958, widened-local half⟩ a `const` declared wider than its initializer holds the initializer's value.
6801
+ if (ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.initializer
6802
+ && (ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)) {
6803
+ seen.add(sym);
6804
+ let ti; try { ti = checker.getTypeAtLocation(d.initializer); } catch { ti = null; }
6805
+ return [...(ti && !anyishType(ti) ? [ti] : []), ...receiverValueTypes(d.initializer, depth + 1, seen)];
6806
+ }
6807
+ if (!ts.isParameter(d)) return [];
6808
+ seen.add(sym);
6809
+ const args = paramArgs.get(sym);
6810
+ if (!args || paramFnEscaped(paramOwnerOf.get(sym))) return [];
6811
+ const out = [];
6812
+ for (const a of args) {
6813
+ if (!a) continue;
6814
+ let ta; try { ta = checker.getTypeAtLocation(a); } catch { ta = null; }
6815
+ if (ta && !anyishType(ta)) out.push(ta); // a VALUE's own type, even `{}`, is a fact about it
6816
+ out.push(...receiverValueTypes(a, depth + 1, seen));
6817
+ }
6818
+ return out;
6819
+ }
6820
+ const dedupeHits = (hits) => { const seen = new Set(); return hits.filter((h) => !seen.has(h.decl) && seen.add(h.decl)); };
6311
6821
  function accessorsAt(propNode, kind /* "get" | "set" */) {
6312
6822
  if (ts.isElementAccessExpression(propNode)) {
6313
6823
  // `c["prop"]` carries no `.name`; resolve the key to the property NAMES it can hold and look each
@@ -6324,9 +6834,19 @@ function accessorsAt(propNode, kind /* "get" | "set" */) {
6324
6834
  // property is common to all of them).
6325
6835
  for (const t of texts) for (const sym of propertySymbolsAcrossArms(recvType, t))
6326
6836
  out.push(...accessorsFromSym(sym, kind));
6327
- return out;
6328
- }
6329
- return accessorsFromSym(checker.getSymbolAtLocation(propNode.name ?? propNode), kind);
6837
+ // R246 — and every type the receiver's VALUE can carry under an assertion or an `any` parameter.
6838
+ for (const vt of receiverValueTypes(propNode.expression))
6839
+ for (const t of texts) for (const sym of propertySymbolsAcrossArms(vt, t)) out.push(...accessorsFromSym(sym, kind));
6840
+ return dedupeHits(out);
6841
+ }
6842
+ const direct = accessorsFromSym(checker.getSymbolAtLocation(propNode.name ?? propNode), kind);
6843
+ if (direct.length || !ts.isPropertyAccessExpression(propNode)) return direct;
6844
+ // R246 — `(s as any).token = v` / `o.token = v` with `o: any`: the checker resolves no symbol at all.
6845
+ const out = [];
6846
+ const name = propNode.name.text;
6847
+ for (const vt of receiverValueTypes(propNode.expression))
6848
+ for (const sym of propertySymbolsAcrossArms(vt, name)) out.push(...accessorsFromSym(sym, kind));
6849
+ return dedupeHits(out);
6330
6850
  }
6331
6851
  // A `Object.defineProperty` descriptor accessor for the forcing site `recv.key` (read → get, assign →
6332
6852
  // set), consulted ONLY when the type-level `accessorsAt` came up empty (the checker types target.key as
@@ -6406,8 +6926,7 @@ function accessorOverrideFanOut(rec, decl, recvExpr) {
6406
6926
  // SOUNDNESS-PRESERVING FALLBACK, the method path's verbatim: a receiver we cannot pin to a LOCAL
6407
6927
  // class (a union, an interface, `any`, an external type) keeps the FULL override set.
6408
6928
  const rootClass = localReceiverClass(recvExpr);
6409
- const direct = rootClass ? allOverrides.filter((om) =>
6410
- ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
6929
+ const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
6411
6930
  const overrides = memberDispatchBodies(decl, rootClass);
6412
6931
  if (direct.length <= CHA_FANOUT_LIMIT) probeR871("accessor", decl, overrides.length - direct.length);
6413
6932
  if (overrides.length === 0) return;
@@ -6658,8 +7177,10 @@ function classBodiedGetter(sym) {
6658
7177
  }
6659
7178
  function enumerateGetters(owner, type, srcExpr) {
6660
7179
  if (!owner) return;
6661
- if (type && type.getProperties) {
6662
- for (const p of propertiesAcrossArms(type)) { // R259 — the disjunction, not the intersection
7180
+ // R246 — the copied VALUE's types too, when its static type is `any`/`unknown` or asserted away.
7181
+ const vprops = srcExpr ? receiverValueTypes(srcExpr).flatMap((vt) => propertiesAcrossArms(vt)) : [];
7182
+ if ((type && type.getProperties) || vprops.length) {
7183
+ for (const p of [...(type?.getProperties ? propertiesAcrossArms(type) : []), ...vprops]) { // R259 — the disjunction, not the intersection
6663
7184
  if (classBodiedGetter(p)) continue; // prototype + non-enumerable → not copied by a spread
6664
7185
  for (const hit of accessorsFromSym(p, "get")) recordAccessorHit(owner, hit, p.getName(), srcExpr);
6665
7186
  }
@@ -6670,9 +7191,7 @@ function enumerateGetters(owner, type, srcExpr) {
6670
7191
  //
6671
7192
  // R120 — THREE UNWRAPS AND ONE ALIAS HOP, because the question is which OBJECT is being copied and
6672
7193
  // 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;
7194
+ const se = unwrapArgExpr(srcExpr); // R780
6676
7195
  if (!se || !ts.isIdentifier(se)) return; // parameter / call return / member
6677
7196
  let sym0 = checker.getSymbolAtLocation(se);
6678
7197
  if (!sym0) return;
@@ -6682,9 +7201,7 @@ function enumerateGetters(owner, type, srcExpr) {
6682
7201
  if (sym0.flags & ts.SymbolFlags.Alias) { try { sym0 = checker.getAliasedSymbol(sym0) ?? sym0; } catch { /* unresolved import */ } }
6683
7202
  for (const d of sym0.declarations ?? []) {
6684
7203
  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;
7204
+ const init = unwrapArgExpr(d.initializer); // R780
6688
7205
  if (!ts.isObjectLiteralExpression(init)) continue;
6689
7206
  for (const pr of init.properties) {
6690
7207
  if (!ts.isGetAccessorDeclaration(pr)) continue;
@@ -6729,8 +7246,7 @@ function enumerateGetters(owner, type, srcExpr) {
6729
7246
  const provenCopiedKeys = (sources) => {
6730
7247
  const keys = new Set();
6731
7248
  for (let s of sources) {
6732
- while (s && (ts.isParenthesizedExpression(s) || ts.isAsExpression(s)
6733
- || ts.isSatisfiesExpression(s) || ts.isNonNullExpression(s))) s = s.expression;
7249
+ s = unwrapArgExpr(s); // R780
6734
7250
  if (!s || !ts.isObjectLiteralExpression(s)) return null; // parameter / call return / variable
6735
7251
  for (const pr of s.properties) {
6736
7252
  if (ts.isSpreadAssignment(pr)) return null; // `{...o}` copies an unknown key set
@@ -6808,12 +7324,15 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
6808
7324
  if (!owner || !targetExpr) return;
6809
7325
  const t = checker.getTypeAtLocation(targetExpr);
6810
7326
  if (!t || !t.getProperties) return;
7327
+ // R246 — every property the target's VALUE can carry: its static type's, and (under an assertion or an
7328
+ // `any`/`unknown` parameter) the types the value really has. One list for both branches below.
7329
+ const tprops = [...propertiesAcrossArms(t), ...receiverValueTypes(targetExpr).flatMap((vt) => propertiesAcrossArms(vt))];
6811
7330
  // `!keys`, not `keys === null`: a Set is always truthy, so this is exactly "the key set is not
6812
7331
  // provable" — and it keeps the tolerance the old `keys && !keys.has(…)` line had for a caller that
6813
7332
  // passes nothing. `keys.has` below is now unguarded, so a future third call site omitting the
6814
7333
  // argument would throw rather than take this branch.
6815
7334
  if (!keys) {
6816
- for (const p of propertiesAcrossArms(t)) { // R259 — the disjunction, not the intersection
7335
+ for (const p of tprops) { // R259 — the disjunction, not the intersection
6817
7336
  if (!accessorsFromSym(p, kind).length) continue;
6818
7337
  if (!keyCouldNameAccessor(p, unprovable.keyType ?? null)) continue;
6819
7338
  const rec = fns.get(owner);
@@ -6823,7 +7342,9 @@ function enumerateTargetAccessors(owner, targetExpr, keys, kind /* "get" | "set"
6823
7342
  }
6824
7343
  return; // a target declaring no reachable accessor discloses nothing (0 real invocations)
6825
7344
  }
6826
- for (const p of propertiesAcrossArms(t)) { // R259 — the disjunction, not the intersection
7345
+ const seenP = new Set();
7346
+ for (const p of tprops) { // R259 — the disjunction, not the intersection
7347
+ if (seenP.has(p)) continue; seenP.add(p);
6827
7348
  // NO `classBodiedGetter`-style exclusion here, and the asymmetry is the point rather than an
6828
7349
  // oversight: R115 excluded a class-bodied GETTER because a prototype accessor is non-enumerable and
6829
7350
  // therefore never COPIED. A prototype SETTER is the opposite — it is found by the assignment's
@@ -6927,6 +7448,29 @@ function decoratorArgUnit(callNode) {
6927
7448
  }
6928
7449
  return qual;
6929
7450
  }
7451
+ // ⟨SOUNDNESS R815⟩ The `extends <expr>` unit of a class that has a constructor unit. The expression runs ONCE, at
7452
+ // class definition (so the evaluating unit edges here, pass 2a′) — and every construction then runs the
7453
+ // constructor of the class it produced through `super`, implicit or explicit, whose body this scan sees only as
7454
+ // the expression's own callees (`mixin()`'s class expression is attributed to `mixin`; a dependency's
7455
+ // `PartialType(X)` is the dependency's ledger/`invisible`). So the constructor edges here too. Attributing the
7456
+ // expression to the constructor ALONE (the old climb) left the definer silent; attributing it to the definer
7457
+ // ALONE drops the super reach — MEASURED: `new D3()` over `class D3 extends Partial(Plain)` from a dependency
7458
+ // that writes in its generated constructor (EXECUTED) went from `invisible:['mixdep']` to ABSENT, and nest's six
7459
+ // `extends PartialType(…)` DTO constructors lost their `@nestjs/mapped-types` disclosure the same way.
7460
+ function heritageUnit(cls, ctorQual) {
7461
+ const qual = ctorQual.replace(/\.constructor$/, ".<heritage>");
7462
+ if (!fns.has(qual)) {
7463
+ const sf = cls.getSourceFile();
7464
+ const h = (cls.heritageClauses ?? []).find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) ?? cls;
7465
+ fns.set(qual, { local: `${fns.get(ctorQual)?.local?.replace(/\.constructor$/, "") ?? "<anonymous>"}.<heritage>`,
7466
+ direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
7467
+ cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(), why: new Set(),
7468
+ entry: false, unitKind: "initializer",
7469
+ loc: `${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(h.getStart()).line + 1}:1`,
7470
+ endLine: sf.getLineAndCharacterOfPosition(h.getEnd()).line + 1 });
7471
+ }
7472
+ return qual;
7473
+ }
6930
7474
  // nearest enclosing analyzed function (closures attribute to it — SEMANTICS §2)
6931
7475
  function enclosing(node) {
6932
7476
  let prev = null;
@@ -6966,6 +7510,42 @@ function enclosing(node) {
6966
7510
  if (ts.isDecorator(p)) return null;
6967
7511
  const n = nodeName.get(p);
6968
7512
  if (n) return n;
7513
+ // ⟨SOUNDNESS R815⟩ THE REST OF THE CLASS-DEFINITION-TIME FAMILY R782/R785 started. Three more pieces of a class
7514
+ // run when the class is EVALUATED, not when it is constructed or a member is called — and the climb
7515
+ // attributed each to a unit that never runs it, leaving the evaluating unit silent (EXECUTED, lane fixture
7516
+ // `fx/r815`: the enclosing function ABSENT, `deny Fs` exit 0; the effect fabricated onto `C.constructor` or the
7517
+ // method):
7518
+ // · a `static x = …` INITIALISER runs at definition, exactly like a `static {}` block, so it lands on the
7519
+ // same `<static-init>` unit (which pass 2a′ wires from the evaluating unit). Climbing on reached the
7520
+ // ClassDeclaration, i.e. `C.constructor` — every `new C()` charged, the definer silent. An instance field
7521
+ // is NOT this (it runs per construction; the constructor is right), and a property that IS a unit
7522
+ // (`static h = () => …` minted as `C.h`) answered above before reaching here.
7523
+ // · an `extends <expr>` HERITAGE expression and
7524
+ // · a COMPUTED member name `[k()]` (class OR object-literal member)
7525
+ // are evaluated in the scope AROUND the class / literal, by whatever evaluates it — so the climb resumes
7526
+ // OUTSIDE the container. (The computed name hung under the member's own node, so the method `C3.[w("k")]`
7527
+ // was charged for its own name.)
7528
+ if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
7529
+ && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
7530
+ && p.parent && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent)))
7531
+ { R815_REACH?.("static"); return staticBlockUnit(p); }
7532
+ // ⟨R1016⟩ an INSTANCE field initialiser of a class expression runs at construction — its constructor unit.
7533
+ if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
7534
+ && !(ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
7535
+ && !ts.isArrowFunction(prev) && !ts.isFunctionExpression(prev)
7536
+ && p.parent && ts.isClassExpression(p.parent) && classExprCtorQual.has(p.parent))
7537
+ return classExprCtorQual.get(p.parent);
7538
+ if (ts.isHeritageClause(p) && p.token === ts.SyntaxKind.ExtendsKeyword && p.parent
7539
+ && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent))) {
7540
+ const ctor = nodeName.get(p.parent);
7541
+ if (ctor) { R815_REACH?.("heritage"); return heritageUnit(p.parent, ctor); }
7542
+ // a class with no constructor unit: the climb already resumes outside it, at the evaluating unit
7543
+ }
7544
+ if (ts.isComputedPropertyName(p) && p.parent?.parent) {
7545
+ const container = p.parent.parent;
7546
+ if (ts.isClassDeclaration(container) || ts.isClassExpression(container)) { R815_REACH?.("ckey"); return enclosing(container.parent); }
7547
+ if (ts.isObjectLiteralExpression(container)) { R815_REACH?.("okey"); return enclosing(container); }
7548
+ }
6969
7549
  // Reached the SourceFile with no named unit: a TOP-LEVEL executable statement. Attribute to the
6970
7550
  // file's synthesized `<module>` initializer unit (minted lazily here) rather than dropping it.
6971
7551
  if (ts.isSourceFile(p)) return moduleUnit(p);
@@ -7204,8 +7784,7 @@ function recordDispatch(rec, decl, pkg) {
7204
7784
  function packageProducedReceiver(recvExpr, pkg) {
7205
7785
  if (!recvExpr || !pkg) return null;
7206
7786
  const unwrap = (x) => {
7207
- while (x && (ts.isAwaitExpression(x) || ts.isParenthesizedExpression(x) || ts.isNonNullExpression(x)
7208
- || ts.isAsExpression(x))) x = x.expression;
7787
+ for (x = unwrapArgExpr(x); x && ts.isAwaitExpression(x);) x = unwrapArgExpr(x.expression); // R780: + `await`
7209
7788
  return x;
7210
7789
  };
7211
7790
  let e = unwrap(recvExpr);
@@ -8357,6 +8936,89 @@ const HOF_INVOKERS = new Set([
8357
8936
  "then", "catch", "finally", "nextTick",
8358
8937
  ]);
8359
8938
 
8939
+ // ⟨SOUNDNESS R803⟩ THE LIST ABOVE IS NO LONGER THE GATE — IT IS THE POSITION MAP FOR THE CALLEES IT NAMES.
8940
+ //
8941
+ // `HOF_INVOKERS` decided, by callee NAME, whether a function reference handed to a non-local callee could be
8942
+ // invoked by it, and every callee it did not name dropped the reference: no edge, no `Unknown`, no row.
8943
+ // Measured on `fx/r803`, every cell EXECUTED by node to run its handler: `e.on`, `e.once`, `e.addListener`,
8944
+ // `process.once`, `http.createServer(h)`, `Array.from(xs, f)`, `new Promise(exec)`, lodash `_.each`/`_.times`,
8945
+ // a dependency `queue.add(f)` and `EventTarget.addEventListener` were all ABSENT, `deny Fs` exit 0 on each
8946
+ // caller. The comment that guarded it called the omission "an honest under-report (sound)" — true about
8947
+ // fabrication, false about silence; it also falsified `KAPPA_PURE`'s premise that lodash/rxjs side effects
8948
+ // "live in visible user callbacks", because this list is what decides whether those callbacks are visible.
8949
+ //
8950
+ // WHY NOT "ASK THE SIGNATURE" ALONE — measured by the ts vein analysis on the 28-entry roster: of 253 sites
8951
+ // where a callee's parameter is callable-typed and the argument is a local function reference, about half are
8952
+ // REMOVALS or STORES (`off`, `removeListener`, `removeEventListener`, `Map.set`, `Object.assign`), and 15 of the
8953
+ // 25 effect-bearing ones never invoke. A signature rule alone fabricates on those.
8954
+ //
8955
+ // So the rule is: an UNLISTED non-local callee invokes every argument position its resolved signature
8956
+ // POSITIVELY declares callable (`calleeParamIsCallable === true`), unless the callee is on the DENYLIST below.
8957
+ // The failure directions, stated because they are the reason for the shape (`candor-denylist-over-allowlist`):
8958
+ // · a non-invoker MISSING from the denylist → an over-charge (its reference's effects charged to the
8959
+ // caller). Visible, and the corpus A/B is the instrument that catches it.
8960
+ // · an invoker the old list missed → now charged. Nothing an invoker can do lands it back in silence
8961
+ // except being DENYLISTED, and the denylist only names declarations whose semantics are a removal or a
8962
+ // store — the removal NAMES on any receiver, and reviewed ES-lib / node-core members by owner.
8963
+ // · a callee whose parameter is `any`/`unknown`/unresolvable (`null`) is NOT admitted here. That is a
8964
+ // residual, named rather than hidden: `console.log(f)`/`assert.equal(f, …)` take `any` and never invoke,
8965
+ // and admitting `null` charges them — priced on the roster before deciding (see the commit message).
8966
+ // The listed names keep their own position map (`hofInvokesArg`/`hofArgIsNeverCallback`) unchanged.
8967
+ //
8968
+ // Removal by NAME on ANY receiver: a method named `removeListener` that invokes the listener it is removing
8969
+ // would be perverse, and these four are the registration API's inverse everywhere they appear (node events,
8970
+ // DOM `EventTarget`, eventemitter3, socket.io's emitter). Asked of the callee's spelling AND of the declaration
8971
+ // it resolved to, because `const processOff = process.off.bind(process); processOff(sig, h)` (vitest) and
8972
+ // got's `removeSessionSocketListener` are the same removal under a local name.
8973
+ //
8974
+ // Everything else is asked of the RESOLVED REVIEWED-LIBRARY declaration (ES lib, @types/node), never of a
8975
+ // name, so a DEPENDENCY's `add(fn)` (p-queue — a real roster silence) is not excused by sharing a name with
8976
+ // `Set.add`. The rule there is the GENERIC VALUE SLOT: a parameter whose declared type is a bare type
8977
+ // parameter (alone or beside non-callable members such as `PromiseLike<T>`) whose constraint has no call
8978
+ // signature. Such a callee is handed a value it knows nothing about and cannot call without a cast — `Map.set`'s
8979
+ // `V`, `Set.add`/`includes`/`indexOf`'s `T`, `Object.assign`/`defineProperties`/`freeze`'s `T`, a promise
8980
+ // executor's `resolve(value: T | PromiseLike<T>)`, `new Proxy(target: T)`, `new WeakRef(target: T)`. The
8981
+ // argument reads callable only because the CALL instantiated `T` with a function type. Restricted to the
8982
+ // reviewed library declarations on purpose: a dependency CAN cast and call (`(x as any)()`), so there the
8983
+ // generic slot is an assumption, and an assumption is not denylist evidence.
8984
+ // ⟨R241⟩ lib.dom interfaces whose every member touches a persistent browser store (see the es-lib call arm).
8985
+ const BROWSER_STORE_IFACES = new Set(["IDBFactory", "IDBDatabase", "IDBObjectStore", "IDBIndex", "IDBCursor",
8986
+ "IDBCursorWithValue", "IDBTransaction", "CacheStorage", "Cache", "StorageManager", "FileSystemHandle",
8987
+ "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemWritableFileStream", "FileSystemSyncAccessHandle", "CookieStore"]);
8988
+ const HOF_REMOVAL_NAMES = new Set(["off", "removeListener", "removeEventListener", "removeAllListeners"]);
8989
+ // NOT non-invokers — they invoke their first argument — but the reflective arm below answers them with the
8990
+ // invoked function's OWN arguments (R947's locator), and this arm, which cannot see those, would add a second
8991
+ // locator-less charge beside it (measured: `Reflect.apply(fs.readFileSync, undefined, ["/tmp/ok/b"])` lost its
8992
+ // `allow Fs /tmp/ok` certification). Owned elsewhere, so excluded here.
8993
+ const HOF_OWNED_ELSEWHERE = new Set(["Reflect.apply", "Reflect.construct"]);
8994
+ const hofOwnerMember = (decl) => {
8995
+ if (!decl) return null;
8996
+ const member = ts.isConstructSignatureDeclaration(decl) ? "new" : decl.name?.getText?.();
8997
+ const holder = decl.parent && ts.isModuleBlock(decl.parent) ? decl.parent.parent : decl.parent; // `namespace Reflect {…}`
8998
+ const owner = holder?.name?.getText?.() ?? null;
8999
+ return owner && member ? `${owner}.${member}` : null;
9000
+ };
9001
+ const declIsReviewedLib = (decl) => !!decl && (declModule(decl) === "<es-lib>" || declIsNodeTypes(decl));
9002
+ const hofCalleeNeverInvokes = (decl, name) => {
9003
+ if ((name && HOF_REMOVAL_NAMES.has(name)) || HOF_REMOVAL_NAMES.has(decl?.name?.getText?.() ?? "")) return true;
9004
+ return declIsReviewedLib(decl) && HOF_OWNED_ELSEWHERE.has(hofOwnerMember(decl) ?? "");
9005
+ };
9006
+ const calleeParamIsGenericSlot = (node, i) => {
9007
+ const sd = checker.getResolvedSignature?.(node)?.declaration;
9008
+ const ps = sd?.parameters;
9009
+ if (!ps?.length || i >= ps.length || !ps[i].type || ps[i].dotDotDotToken) return false;
9010
+ const t = checker.getTypeFromTypeNode(ps[i].type);
9011
+ const isFreeTP = (u) => !!(u.flags & ts.TypeFlags.TypeParameter)
9012
+ && !(checker.getBaseConstraintOfType(u)?.getCallSignatures?.().length > 0);
9013
+ const parts = t.types ?? [t];
9014
+ return parts.some(isFreeTP) && !parts.some((u) => !isFreeTP(u) && u.getCallSignatures?.().length > 0);
9015
+ };
9016
+ // ⟨R1031⟩ reviewed-library callees whose object-literal argument's callable members are INSTALLED, not
9017
+ // invoked: a property descriptor (`get`/`set`) or a descriptor map. Asked of the resolved declaration.
9018
+ const OBJ_ARG_INSTALLS = new Set(["ObjectConstructor.defineProperty", "ObjectConstructor.defineProperties",
9019
+ "ObjectConstructor.create", "Reflect.defineProperty"]);
9020
+ const HOF_REACH = process.env.CANDOR_HOF_REACH ? (k, n) => console.error(`HOF-REACH ${k} ${n}`) : null;
9021
+
8360
9022
  // ---- process.env recognition: the direct dot access (`process.env.KEY`) is the JVM System.getenv twin,
8361
9023
  // but the same environment READ is spelled several other ways that all read silent-pure without help:
8362
9024
  // bracket access (`process.env[k]`), a local const-alias (`const env = process.env; env.KEY`),
@@ -8387,14 +9049,25 @@ const declImportsNodeProcess = (decl) => {
8387
9049
  // downstream consult — the fix in one place, per brief §F1 item 3 ("make the two paths share one
8388
9050
  // authority", not patch the losing copy). Populated by the pre-pass immediately below.
8389
9051
  const processAliasSymbols = new Set();
8390
- const identIsGlobalProcess = (id) => {
9052
+ const identIsGlobalProcess = (id0) => {
9053
+ // ⟨SOUNDNESS R936⟩ through the ONE transparent-wrapper set (R780): `(process as any).argv`, `(<any>process).argv`
9054
+ // and even `(process).argv` were ABSENT because the argv arm handed this the wrapped node and this matched an
9055
+ // Identifier only — the env arm unwrapped first and the argv arm did not. Unwrapping HERE gives every caller
9056
+ // the same answer instead of each remembering to.
9057
+ const id = unwrapArgExpr(id0);
8391
9058
  if (!id) return false;
9059
+ // ⟨SOUNDNESS R936⟩ `require("process")` / `require("node:process")` IS the process object — the module-binding
9060
+ // spelling of the same global (CJS, `--allow-js` trees, and `createRequire`). `require("process").env.HOME` read
9061
+ // nothing (EXECUTED). Keyed on a callee spelled `require` and the literal specifier; a project function that
9062
+ // happens to be called `require` and returns something else is the only false match, and it over-charges.
9063
+ if (ts.isCallExpression(id) && ts.isIdentifier(id.expression) && id.expression.text === "require"
9064
+ && id.arguments.length === 1 && ts.isStringLiteralLike(id.arguments[0])
9065
+ && (id.arguments[0].text === "process" || id.arguments[0].text === "node:process")) return true;
8392
9066
  // `globalThis.process` / `global.process` — the SAME process object reached off the global (isomorphic code:
8393
9067
  // `globalThis.process?.env`, often `(globalThis as any).process.env`). Unwrap parens/`as` casts around the
8394
9068
  // root; the `globalThis`/`global` root must be the ambient global, not a project shadow.
8395
9069
  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;
9070
+ const root = unwrapArgExpr(id.expression); // R780: `(<any>globalThis).process` is the same object
8398
9071
  if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
8399
9072
  const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
8400
9073
  return !gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
@@ -8423,9 +9096,7 @@ const identIsGlobalProcess = (id) => {
8423
9096
  {
8424
9097
  const isGlobalProcessInitializer = (expr) => {
8425
9098
  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`
9099
+ return identIsGlobalProcess(unwrapArgExpr(expr)); // R780 // handles `globalThis.process` / `global.process` / bare ambient `process`
8429
9100
  };
8430
9101
  const collectProcessAliases = (node) => {
8431
9102
  if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.initializer) {
@@ -8434,8 +9105,7 @@ const identIsGlobalProcess = (id) => {
8434
9105
  if (sym) processAliasSymbols.add(sym);
8435
9106
  }
8436
9107
  } 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;
9108
+ const root = unwrapArgExpr(node.initializer); // R780
8439
9109
  if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
8440
9110
  const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
8441
9111
  if (!gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
@@ -8476,11 +9146,7 @@ const identIsGlobalProcess = (id) => {
8476
9146
  // EXPRESSION — that is the model this adopts, with identity widened from a node to a value.
8477
9147
  //
8478
9148
  // 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
- };
9149
+ const unwrapEnvTransparent = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a fourth copy of it
8484
9150
  // `process.env` / `process["env"]` / `globalThis.process?.env` / `(process as any).env` — the global
8485
9151
  // process object's `env` member. The `process` side keeps `identIsGlobalProcess`'s shadow guard, so a
8486
9152
  // project-local `process` (a parameter, a module `const`) matches nothing.
@@ -8536,16 +9202,159 @@ const envValueLeaves = (expr, out = []) => {
8536
9202
  // READ was not even `Unknown` — it was absent).
8537
9203
  // A binding is a `VariableDeclaration` with an identifier name (no initializer = a binding with no env
8538
9204
  // 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).
9205
+ // `{ env: e } = process` (wrappers unwrapped).
9206
+ //
9207
+ // ⟨R934/R935⟩ THE SAME QUESTION OVER EVERY STORAGE LOCATION WHOSE BINDINGS THIS PASS CAN ENUMERATE, not
9208
+ // over locals only. The identity above was asked of a VARIABLE; the environment object also travels
9209
+ // through a PARAMETER (R934: `function rd(e) { return e.K }` called as `rd(process.env)` — the read runs
9210
+ // in `rd`'s frame and `rd` was absent) and through a heap FIELD (R935: `const cfg = { env: process.env }`
9211
+ // at module level, read later as `cfg.env.K` by a function that never calls the module initializer —
9212
+ // absent; only `<module>` carried Env). Both are one defect: the alias model stopped at the first
9213
+ // location that was not a local, so the value was lost the moment it was stored anywhere else. The fix
9214
+ // is not a second analysis per location kind; it is the SAME binding table and the SAME two fixpoints
9215
+ // with two more kinds of location in them:
9216
+ //
9217
+ // PARAMETER — its bindings are the arguments at every VISIBLE call site (positional, up to a spread;
9218
+ // a site that omits it binds its default, else `undefined`). It joins MUST only — every visible site
9219
+ // hands it the environment — and NEVER MAY. That asymmetry is pass 2b's ruling, transferred: a
9220
+ // parameter fed the environment at one site and `{}` at another is the DIVERGENT shared-HOF shape,
9221
+ // and charging the callee would pool the read onto the caller that passed `{}` (a fabrication). The
9222
+ // caller that passes the environment is already charged by the Env arm (a call argument is not an
9223
+ // inert consumer), so every gate above the callee still fires — the callee's own scoped gate is the
9224
+ // only one that stays 0, exactly as it does for a divergent HOF. Only functions whose every reference
9225
+ // is a CALL are eligible: a function that escapes as a value (a callback, `.call`, `.bind`, stored)
9226
+ // has bindings this pass cannot see. Methods are eligible only on a class with no heritage that no
9227
+ // project class extends — a dispatched call resolves to the base declaration, and an override's
9228
+ // parameter would otherwise look fed by fewer sites than it is.
9229
+ // PROPERTY — its bindings are the initializer of every VALUE-BEARING declaration (an object-literal
9230
+ // property, a class field, a constructor parameter property — which is bound exactly as its
9231
+ // parameter is) and the right side of every assignment to it. An absent initializer is not a binding:
9232
+ // a field that is `undefined` until it is assigned the environment cannot make a reader read a
9233
+ // DIFFERENT object's keys, and that is the only thing MUST has to rule out. MUST charges a reader Env;
9234
+ // MAY (rebound to something else somewhere) discloses `Unknown[env-maybe-read]`, the local-alias
9235
+ // posture, because unlike a parameter there is no caller in the reader's chain that carries it. A
9236
+ // property declared by an interface or a type literal (`PropertySignature`) is NOT a location here:
9237
+ // any conforming object can be its value, so its bindings are an open set — the residual is stated
9238
+ // in SOUNDNESS, not hidden. A field of a class some project class extends is excluded for the
9239
+ // override reason above.
9240
+ //
9241
+ // Writing the environment INTO a property stays charged where it happens (the Env arm's "store" rule);
9242
+ // this adds the READERS and removes nothing.
8541
9243
  const envAliasSymbols = new Set();
8542
9244
  const envMayAliasSymbols = new Set();
9245
+ // ⟨R935⟩ property locations, kept apart from the local sets so a property name never reaches the
9246
+ // identifier-keyed consumers (2c's `envSourceKind`, the alias-binding inert rule) by accident.
9247
+ const envPropMustSymbols = new Set();
9248
+ const envPropMaySymbols = new Set();
9249
+ // ⟨R935⟩ a property location's identity: its first declaration node (see `memberSymOf` for why not the symbol).
9250
+ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
9251
+ // ⟨SOUNDNESS R935, the RECEIVER half⟩ THE LOCATION A MEMBER READ NAMES IS THE OBJECT THE RECEIVER HOLDS, not the
9252
+ // member its DECLARED TYPE declares. The first cut keyed a read on the checker's property symbol, which is the
9253
+ // literal's own property only when the binding's type was INFERRED from the literal. Give the binding a type —
9254
+ // `const icfg: Cfg = { env: process.env }` — and `icfg.env` resolves to `Cfg`'s `PropertySignature`, an open set
9255
+ // that is no location at all, so `viaIface() { return icfg.env.SECRET }` was ABSENT with `deny Env viaIface` exit
9256
+ // 0 over a read that printed the planted secret (EXECUTED). An ARRAY literal had no location to key at all:
9257
+ // `const arr = [process.env]; arr[0].SECRET`, same measurement. Where the receiver is an identifier bound by a
9258
+ // `const` to an object or array literal, the object IS that literal, so the read is keyed on the literal's own
9259
+ // member — the last property of that name (a later spread makes it unresolvable), or the element at a literal
9260
+ // index with no spread or hole before it. Everything that can REBIND such a member off the books is a binding
9261
+ // the fixpoint must see, or the location is charged over a replaced value:
9262
+ // · an OBJECT literal's member is written through its declared member too (`icfg.env = {}` names `Cfg.env`,
9263
+ // as does any other `Cfg`-typed object), so a read resolved this way is MAY wherever that declared member
9264
+ // is ever written or reflectively written (`envDeclWritten`, filled by the binding walk below);
9265
+ // · an ARRAY's elements move under any method call or escape, so an element is MUST only while every
9266
+ // reference to the array is a literal-index READ; any other reference makes every element MAY.
9267
+ // MAY discloses `Unknown[env-maybe-read]`, as every other location does. A `let` binding, a receiver that is a
9268
+ // parameter or a property, and a read through iteration (`for…of`, `.map`) are not resolved here.
9269
+ const envDeclWritten = new Set();
9270
+ const envLitElems = new Map(); // array-literal element node -> true while every reference is an index read
9271
+ const envConstLiteral = (id) => {
9272
+ if (!id || !ts.isIdentifier(id)) return null;
9273
+ let sym = checker.getSymbolAtLocation(id);
9274
+ if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { return null; } }
9275
+ const d = sym?.valueDeclaration;
9276
+ if (!d || !ts.isVariableDeclaration(d) || !ts.isIdentifier(d.name) || !d.initializer
9277
+ || !(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)
9278
+ || !projectFiles.has(path.resolve(d.getSourceFile().fileName))) return null;
9279
+ const lit = unwrapArgExpr(d.initializer);
9280
+ return lit && (ts.isObjectLiteralExpression(lit) || ts.isArrayLiteralExpression(lit)) ? lit : null;
9281
+ };
9282
+ const envMemberName = (p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteralLike(p.name)
9283
+ || ts.isNumericLiteral(p.name) || ts.isPrivateIdentifier(p.name))) ? p.name.text : null;
9284
+ // -> { key, declKey } for a member read whose receiver is a const literal, else null.
9285
+ const envLiteralMember = (n) => {
9286
+ let recv, name = null, idx = null;
9287
+ if (ts.isPropertyAccessExpression(n)) { recv = n.expression; name = n.name.text; }
9288
+ else if (ts.isElementAccessExpression(n) && n.argumentExpression) {
9289
+ recv = n.expression;
9290
+ if (ts.isStringLiteralLike(n.argumentExpression)) name = n.argumentExpression.text;
9291
+ else if (ts.isNumericLiteral(n.argumentExpression)) idx = Number(n.argumentExpression.text);
9292
+ else return null;
9293
+ } else return null;
9294
+ const lit = envConstLiteral(unwrapArgExpr(recv));
9295
+ if (!lit) return null;
9296
+ if (ts.isArrayLiteralExpression(lit)) {
9297
+ if (idx === null || !Number.isInteger(idx) || idx >= lit.elements.length) return null;
9298
+ for (let i = 0; i <= idx; i++) {
9299
+ const el = lit.elements[i];
9300
+ if (ts.isSpreadElement(el) || ts.isOmittedExpression(el)) return null;
9301
+ }
9302
+ return { key: lit.elements[idx], declKey: null };
9303
+ }
9304
+ const want = name ?? (idx !== null ? String(idx) : null);
9305
+ if (want === null) return null;
9306
+ let hit = null;
9307
+ for (const p of lit.properties) {
9308
+ if (ts.isSpreadAssignment(p)) hit = p;
9309
+ else if (envMemberName(p) === want) hit = p;
9310
+ }
9311
+ if (!hit || !(ts.isPropertyAssignment(hit) || ts.isShorthandPropertyAssignment(hit))) return null;
9312
+ const key = envLocKey(checker.getSymbolAtLocation(hit.name));
9313
+ if (!key) return null;
9314
+ let raw = null;
9315
+ try { raw = ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
9316
+ : (n.argumentExpression ? checker.getSymbolAtLocation(n.argumentExpression) : null); } catch { raw = null; }
9317
+ return { key, declKey: envLocKey(raw) };
9318
+ };
8543
9319
  {
8544
9320
  const ENV_DESTRUCTURED = Symbol("env-destructured");
8545
- const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED]
9321
+ const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED | { same: symbol }]
8546
9322
  const note = (sym, v) => { if (sym) (bindings.get(sym) ?? bindings.set(sym, []).get(sym)).push(v); };
8547
9323
  const LOGICAL_ASSIGN = new Set([ts.SyntaxKind.EqualsToken, ts.SyntaxKind.QuestionQuestionEqualsToken,
8548
9324
  ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken]);
9325
+ const paramSyms = new Set(); // ⟨R934⟩ parameters with at least one recorded binding
9326
+ const propSyms = new Set(); // ⟨R935⟩ eligible property locations with at least one binding
9327
+ const propIneligible = new Set();
9328
+ // The LOCATION KEY of a member reference: `o.p` / `o["p"]` → the property's first declaration node.
9329
+ // Keyed by DECLARATION, not symbol: an object literal's type is WIDENED when it is bound, and the
9330
+ // widened type carries fresh transient property symbols — `cfg.env` at a read site and `env:` in the
9331
+ // literal are two symbol objects for one declaration (measured: the read never matched the binding).
9332
+ const memberRawSym = (n) => (ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
9333
+ : (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
9334
+ ? checker.getSymbolAtLocation(n.argumentExpression) : null) ?? null;
9335
+ const memberSymOf = (n) => envLiteralMember(n)?.key ?? envLocKey(memberRawSym(n));
9336
+ // Is this property symbol a location whose bindings we can enumerate? Every declaration must be
9337
+ // value-bearing, in a project file, and not a member of an extended class.
9338
+ const propEligible = (sym) => {
9339
+ if (!sym || propIneligible.has(sym)) return false;
9340
+ const decls = sym.declarations ?? [];
9341
+ const ok = decls.length > 0 && decls.every((d) => {
9342
+ if (!projectFiles.has(path.resolve(d.getSourceFile().fileName))) return false;
9343
+ if (ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) return ts.isObjectLiteralExpression(d.parent);
9344
+ if (ts.isPropertyDeclaration(d)) return !d.modifiers?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword)
9345
+ && !extendedClassSyms.has(classSymOf(d.parent));
9346
+ if (ts.isParameter(d)) return ts.isConstructorDeclaration(d.parent) && !extendedClassSyms.has(classSymOf(d.parent.parent));
9347
+ return false;
9348
+ });
9349
+ if (!ok) propIneligible.add(sym);
9350
+ return ok;
9351
+ };
9352
+ const noteProp = (sym, v) => {
9353
+ if (propEligible(sym)) { const k = envLocKey(sym); propSyms.add(k); note(k, v); }
9354
+ else if (sym && v !== undefined) envDeclWritten.add(envLocKey(sym)); // ⟨R935⟩ see `envLiteralMember`
9355
+ };
9356
+ // ⟨R935⟩ a WRITE through a member that is not itself a location still rebinds whatever object it lands on.
9357
+ const noteWrite = (sym, v) => { if (sym) { envDeclWritten.add(envLocKey(sym)); noteProp(sym, v); } };
8549
9358
  const collect = (node) => {
8550
9359
  if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
8551
9360
  note(checker.getSymbolAtLocation(node.name), node.initializer ?? null);
@@ -8558,28 +9367,166 @@ const envMayAliasSymbols = new Set();
8558
9367
  }
8559
9368
  } else if (ts.isBinaryExpression(node) && LOGICAL_ASSIGN.has(node.operatorToken.kind) && ts.isIdentifier(node.left)) {
8560
9369
  note(checker.getSymbolAtLocation(node.left), node.right);
9370
+ } else if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
9371
+ && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
9372
+ && (ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left))) {
9373
+ // ⟨R935⟩ an assignment to a property location; a compound operator (`+=`) binds a non-environment.
9374
+ const s = memberRawSym(node.left);
9375
+ if (s) noteWrite(s, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null);
9376
+ // ⟨R935⟩ …and the const literal's own member, when the receiver resolves to one.
9377
+ const lm = envLiteralMember(node.left);
9378
+ if (lm && !ts.isArrayLiteralExpression(lm.key.parent)) { propSyms.add(lm.key); note(lm.key, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null); }
9379
+ } else if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
9380
+ const s = checker.getSymbolAtLocation(node.name);
9381
+ if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, node.initializer);
9382
+ } else if (ts.isShorthandPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
9383
+ const s = checker.getSymbolAtLocation(node.name);
9384
+ const v = checker.getShorthandAssignmentValueSymbol(node);
9385
+ if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, v ? { same: v } : null);
9386
+ } else if (ts.isPropertyDeclaration(node) && node.initializer && node.name) {
9387
+ noteProp(checker.getSymbolAtLocation(node.name), node.initializer);
9388
+ } else if (ts.isParameter(node) && ts.isIdentifier(node.name) && ts.isConstructorDeclaration(node.parent)
9389
+ && ts.getModifiers?.(node)?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword
9390
+ || m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword
9391
+ || m.kind === ts.SyntaxKind.ReadonlyKeyword)) {
9392
+ // a constructor parameter property is bound exactly as its parameter is
9393
+ const [ps, fs] = checker.getSymbolsOfParameterPropertyDeclaration(node, node.name.text);
9394
+ if (ps && fs) noteProp(fs, { same: ps });
9395
+ }
9396
+ // ⟨R935⟩ WRITES THE TABLE ABOVE CANNOT NAME: a reflective write into the object (`Object.assign(o, …)`,
9397
+ // `Object.defineProperty`, `Reflect.set`) or a computed-key store `o[k] = v` can rebind ANY of the
9398
+ // object's properties. Every eligible property of the target's type gets a non-environment binding,
9399
+ // so a location written that way can be MAY at best — a reader discloses `Unknown` rather than being
9400
+ // charged `Env` over a value that was replaced off the books (executed: an `Object.assign` rebind read
9401
+ // the replacement, and the first cut of this charged it `Env`).
9402
+ const reflectTarget = ts.isCallExpression(node)
9403
+ && /^(Object\.(assign|defineProperty|defineProperties)|Reflect\.(set|defineProperty))$/.test(
9404
+ (ts.isPropertyAccessExpression(node.expression) ? node.expression.getText() : "").replace(/\s+/g, ""))
9405
+ ? node.arguments[0]
9406
+ : (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
9407
+ && ts.isElementAccessExpression(node.left) && node.left.argumentExpression
9408
+ && !ts.isStringLiteralLike(node.left.argumentExpression) && !ts.isNumericLiteral(node.left.argumentExpression))
9409
+ ? node.left.expression : null;
9410
+ if (reflectTarget) {
9411
+ let props = [];
9412
+ try { props = checker.getTypeAtLocation(reflectTarget)?.getProperties?.() ?? []; } catch { props = []; }
9413
+ for (const ps of props) { envDeclWritten.add(envLocKey(ps)); if (propEligible(ps)) noteProp(ps, null); }
9414
+ // ⟨R935⟩ a const object literal written reflectively: every member of the literal itself is rebound.
9415
+ const tl = envConstLiteral(unwrapArgExpr(reflectTarget));
9416
+ if (tl && ts.isObjectLiteralExpression(tl)) for (const p of tl.properties) {
9417
+ const k = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) ? envLocKey(checker.getSymbolAtLocation(p.name)) : null;
9418
+ if (k) { propSyms.add(k); note(k, null); }
9419
+ }
9420
+ }
9421
+ // ⟨R935⟩ an ARRAY literal bound by a `const`: each element is a location holding its own value.
9422
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer
9423
+ && (ts.getCombinedNodeFlags(node) & ts.NodeFlags.Const)) {
9424
+ const al = unwrapArgExpr(node.initializer);
9425
+ if (al && ts.isArrayLiteralExpression(al)) {
9426
+ for (const el of al.elements) if (!ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) {
9427
+ propSyms.add(el); note(el, el); envLitElems.set(el, al);
9428
+ }
9429
+ }
8561
9430
  }
8562
9431
  ts.forEachChild(node, collect);
8563
9432
  };
8564
9433
  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".
9434
+ // ⟨R935⟩ an array literal's elements are MUST only while every reference to the array is a literal-index READ.
9435
+ if (envLitElems.size) {
9436
+ const arrs = new Set(envLitElems.values());
9437
+ const names = new Set([...arrs].map((a) => a.parent?.name?.text).filter(Boolean));
9438
+ const opened = new Set();
9439
+ const visitRefs = (node) => {
9440
+ if (ts.isIdentifier(node) && names.has(node.text)) {
9441
+ const lit = envConstLiteral(node);
9442
+ if (lit && arrs.has(lit) && !opened.has(lit) && !(lit.parent?.name === node)) {
9443
+ let cur = node;
9444
+ while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
9445
+ || ts.isAsExpression(cur.parent) || ts.isSatisfiesExpression?.(cur.parent))) cur = cur.parent;
9446
+ const p = cur.parent;
9447
+ const indexRead = p && ts.isElementAccessExpression(p) && p.expression === cur && p.argumentExpression
9448
+ && ts.isNumericLiteral(p.argumentExpression)
9449
+ && !(p.parent && ts.isBinaryExpression(p.parent) && p.parent.left === p && p.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
9450
+ && !(p.parent && (ts.isDeleteExpression(p.parent) || ts.isPrefixUnaryExpression(p.parent) || ts.isPostfixUnaryExpression(p.parent)))
9451
+ && !isDestructuringAssignTarget(p);
9452
+ const typeOnly = p && (ts.isTypeQueryNode(p) || ts.isExportSpecifier(p));
9453
+ if (!indexRead && !typeOnly) opened.add(lit);
9454
+ }
9455
+ }
9456
+ ts.forEachChild(node, visitRefs);
9457
+ };
9458
+ for (const sf of sources) visitRefs(sf);
9459
+ for (const [el, al] of envLitElems) if (opened.has(al)) note(el, null);
9460
+ }
9461
+ // ⟨R934⟩ the parameter bindings come from the shared PARAMETER PROVENANCE index above. What `collect` noted for
9462
+ // a parameter BEFORE this line is its in-body REASSIGNMENTS (`e = {}`); kept apart, because the two kinds of
9463
+ // binding answer different questions below.
9464
+ const paramAssigns = new Map();
9465
+ for (const [ps, vals] of paramArgs) {
9466
+ paramSyms.add(ps); paramAssigns.set(ps, [...(bindings.get(ps) ?? [])]);
9467
+ for (const v of vals) note(ps, v);
9468
+ }
9469
+ const leafIn = (leaf, set, propSet) => isProcessEnvExpr(leaf)
9470
+ || (ts.isIdentifier(leaf) && set.has(checker.getSymbolAtLocation(leaf)))
9471
+ || (!!propSet && propSet.has(memberSymOf(leaf)) && !(propSet === envPropMustSymbols && envLitDowngraded(leaf)));
9472
+ // ⟨R935⟩ a literal member read through a receiver whose DECLARED member is written elsewhere is MAY at best.
9473
+ function envLitDowngraded(n) {
9474
+ const lm = (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) ? envLiteralMember(n) : null;
9475
+ return !!lm && !!lm.declKey && lm.declKey !== lm.key && envDeclWritten.has(lm.declKey);
9476
+ }
9477
+ const hasEnvLeaf = (v, set, propSet) => v === ENV_DESTRUCTURED
9478
+ || (!!v && typeof v === "object" && "same" in v ? set.has(v.same) || propSet?.has(v.same) === true
9479
+ : (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set, propSet))));
9480
+
9481
+
9482
+ // MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS, PROPERTIES
9483
+ // and (⟨R934⟩, see MUST below) PARAMETERS: a property's MAY reads a local's MAY and vice versa.
8569
9484
  for (let changed = true, n = 0; changed && n < 64; n++) {
8570
9485
  changed = false;
8571
9486
  for (const [sym, vals] of bindings) {
8572
- if (!envMayAliasSymbols.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols))) { envMayAliasSymbols.add(sym); changed = true; }
9487
+ const isProp = propSyms.has(sym);
9488
+ const set = isProp ? envPropMaySymbols : envMayAliasSymbols;
9489
+ if (!set.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols, envPropMaySymbols))) { set.add(sym); changed = true; }
8573
9490
  }
8574
9491
  }
8575
9492
  // 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;
9493
+ // least, so a cycle that never touches process.env directly stays MAY (the disclosing side). Locals and
9494
+ // properties start from their MAY sets.
9495
+ //
9496
+ // ⟨SOUNDNESS R934, the DIVERGENT half⟩ A PARAMETER IS THE ENVIRONMENT IN ITS READER'S FRAME WHEN *SOME* VISIBLE
9497
+ // CALL SITE HANDS IT THE ENVIRONMENT — not every one. The first cut of R934 required every site, citing pass
9498
+ // 2b's divergent-HOF ruling, and left `readDiv(e) { return e.SECRET }` called as `readDiv(process.env)` and
9499
+ // `readDiv({…})` ABSENT with `deny Env readDiv` exit 0 over a read that printed the planted secret (EXECUTED).
9500
+ // 2b's ruling does not transfer: it is about a CALLBACK, whose effect is another unit's body, so the HOF
9501
+ // performs nothing itself and pooling the callbacks onto it would invent an effect no execution of the HOF's
9502
+ // own code performs. Here the read is in the callee's OWN body, and an execution in which it reads the
9503
+ // environment exists — that is the all-paths merge `envValueLeaves` already charges for `(o ?? process.env).X`.
9504
+ // Pass 2c, in this same file, already answers the WRITE side this way (`envFed` is set by ANY site, and the
9505
+ // writer is charged `Env`), so the read side requiring every site was the same question answered twice.
9506
+ // The price is the one every summary pays: a caller handing the callee `{}` inherits `Env` through the
9507
+ // ordinary edge, exactly as a caller of 2c's `populate` and of an `??`-merging reader already does.
9508
+ // Because SOME suffices, whether the function escapes no longer matters — hidden sites can only add bindings.
9509
+ // An in-body REASSIGNMENT (`e = {}`) is the flow-insensitive rebind the local-alias rule exists for: the
9510
+ // parameter is then MUST only if every reassignment is also the environment, and otherwise MAY, which
9511
+ // discloses `Unknown[env-maybe-read]` on the reader rather than charging over a value that was replaced.
9512
+ const mustTry = () => {
9513
+ let changed = false;
8579
9514
  for (const sym of envMayAliasSymbols) {
8580
- if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols))) { envAliasSymbols.add(sym); changed = true; }
9515
+ if (paramSyms.has(sym)) continue;
9516
+ if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envAliasSymbols.add(sym); changed = true; }
8581
9517
  }
8582
- }
9518
+ for (const sym of envPropMaySymbols) {
9519
+ if (!envPropMustSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envPropMustSymbols.add(sym); changed = true; }
9520
+ }
9521
+ for (const sym of paramSyms) {
9522
+ if (envAliasSymbols.has(sym)) continue;
9523
+ const must = (v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols);
9524
+ if (!(paramArgs.get(sym) ?? []).some(must) || !(paramAssigns.get(sym) ?? []).every(must)) continue;
9525
+ envAliasSymbols.add(sym); changed = true;
9526
+ }
9527
+ return changed;
9528
+ };
9529
+ for (let n = 0; mustTry() && n < 64; n++) { /* to fixpoint */ }
8583
9530
  }
8584
9531
  // True when `id` is an identifier resolving to a confirmed process.env alias local.
8585
9532
  const identIsEnvAlias = (id) => {
@@ -8595,11 +9542,32 @@ const identIsEnvMayAlias = (id) => {
8595
9542
  };
8596
9543
  // The env kind of an expression's VALUE: "env" if some leaf is process.env or a MUST-alias, "may" if
8597
9544
  // some leaf is only a MAY-alias, else null. The one answer the Env arm, 2c and the shared helpers ask.
9545
+ // ⟨R935⟩ a member reference `o.p` / `o["p"]` whose property is an environment location.
9546
+ const envMemberSym = (n) => {
9547
+ if (!(envPropMaySymbols.size > 0)) return null;
9548
+ const lm = envLiteralMember(n); // ⟨R935⟩ the object the receiver holds, before the member its type declares
9549
+ if (lm && envPropMaySymbols.has(lm.key)) return lm.key;
9550
+ if (ts.isPropertyAccessExpression(n)) return envLocKey(checker.getSymbolAtLocation(n.name));
9551
+ if (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
9552
+ return envLocKey(checker.getSymbolAtLocation(n.argumentExpression));
9553
+ return null;
9554
+ };
9555
+ const envPropKind = (n) => {
9556
+ const s = envMemberSym(n);
9557
+ if (!s || !envPropMaySymbols.has(s)) return null;
9558
+ if (!envPropMustSymbols.has(s)) return "may";
9559
+ // ⟨R935⟩ resolved through the receiver: MAY wherever the declared member is written off the literal.
9560
+ const lm = envLiteralMember(n);
9561
+ if (lm && lm.key === s && lm.declKey && lm.declKey !== s && envDeclWritten.has(lm.declKey)) return "may";
9562
+ return "env";
9563
+ };
8598
9564
  const envValueKind = (expr) => {
8599
9565
  let kind = null;
8600
9566
  for (const l of envValueLeaves(expr)) {
8601
9567
  if (isProcessEnvExpr(l) || identIsEnvAlias(l)) return "env";
8602
- if (identIsEnvMayAlias(l)) kind = "may";
9568
+ const pk = envPropKind(l);
9569
+ if (pk === "env") return "env";
9570
+ if (identIsEnvMayAlias(l) || pk === "may") kind = "may";
8603
9571
  }
8604
9572
  return kind;
8605
9573
  };
@@ -8612,8 +9580,21 @@ const envIsAssignOp = (k) => k >= ts.SyntaxKind.FirstAssignment && k <= ts.Synta
8612
9580
  let envAliasNames = null;
8613
9581
  const envRefKind = (node) => {
8614
9582
  if (isProcessEnvExpr(node)) return "env";
9583
+ // ⟨R935⟩ a READ of an environment property location. Not an assignment target, a `delete` operand, a
9584
+ // destructuring target or a `++`/`--` operand: those write the location, they do not read the object.
9585
+ if (envPropMaySymbols.size > 0 && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))) {
9586
+ const pk = envPropKind(node);
9587
+ if (!pk) return null;
9588
+ const p = node.parent;
9589
+ if (p && ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
9590
+ if (p && (ts.isDeleteExpression(p) || ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
9591
+ && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)))) return null;
9592
+ if (isDestructuringAssignTarget(node)) return null;
9593
+ return pk;
9594
+ }
8615
9595
  if (!ts.isIdentifier(node)) return null;
8616
- envAliasNames ??= new Set([...envMayAliasSymbols].map((s) => s.name));
9596
+ // ⟨R934⟩ MUST-parameters are in `envAliasSymbols` but never in the MAY set, so the name filter reads both.
9597
+ envAliasNames ??= new Set([...envMayAliasSymbols, ...envAliasSymbols].map((s) => s.name));
8617
9598
  if (!envAliasNames.has(node.text)) return null;
8618
9599
  const p = node.parent;
8619
9600
  if (!p) return null;
@@ -8628,8 +9609,9 @@ const envRefKind = (node) => {
8628
9609
  && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return null;
8629
9610
  const sym = ts.isShorthandPropertyAssignment(p) && p.name === node
8630
9611
  ? checker.getShorthandAssignmentValueSymbol(p) : checker.getSymbolAtLocation(node);
8631
- if (!sym || !envMayAliasSymbols.has(sym)) return null;
8632
- return envAliasSymbols.has(sym) ? "env" : "may";
9612
+ if (!sym) return null;
9613
+ if (envAliasSymbols.has(sym)) return "env";
9614
+ return envMayAliasSymbols.has(sym) ? "may" : null;
8633
9615
  };
8634
9616
  // ⟨R928/R804⟩ Does the CONSUMER of this env value provably touch no key? Climb through what passes the
8635
9617
  // value on unchanged (the transparent wrappers, both arms of `?:`, either side of `??`/`||`, the right of
@@ -8761,14 +9743,10 @@ const globalBuiltinCallee = (callee) => {
8761
9743
  if (!callee || !ts.isPropertyAccessExpression(callee)) return null;
8762
9744
  const member = callee.name?.text;
8763
9745
  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;
9746
+ const owner = unwrapArgExpr(callee.expression); // R780
8767
9747
  if (ts.isIdentifier(owner)) return identIsGlobal(owner) ? `${owner.text}.${member}` : null;
8768
9748
  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;
9749
+ const root = unwrapArgExpr(owner.expression); // R780
8772
9750
  if (!ts.isIdentifier(root) || !GLOBAL_ROOTS.has(root.text) || !identIsGlobal(root)) return null;
8773
9751
  return `${owner.name.text}.${member}`;
8774
9752
  };
@@ -8805,9 +9783,7 @@ const globalBareCallee = (callee) => {
8805
9783
  if (!callee) return null;
8806
9784
  if (ts.isIdentifier(callee)) return identIsGlobal(callee) ? callee.text : null;
8807
9785
  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;
9786
+ const root = unwrapArgExpr(callee.expression); // R780
8811
9787
  // ONLY a global ROOT, never an arbitrary owner: `Object.assign` must not read as the bare global
8812
9788
  // `assign`, and `globalThis.Object.assign` must not read as the bare global `Object` — both are the
8813
9789
  // member helper's business, and answering them here would be the second copy §G exists to prevent.
@@ -9021,6 +9997,12 @@ function visitCalls(node) {
9021
9997
  }
9022
9998
  const sig = checker.getResolvedSignature(node);
9023
9999
  let decl = sig && sig.declaration;
10000
+ // ⟨R1016⟩ a construction of a class expression runs its constructor unit, whatever the signature names.
10001
+ const classExprCtor = rec ? classExprCtorOfNew(node) : null;
10002
+ if (classExprCtor && classExprCtor !== owner) {
10003
+ rec.edges.add(classExprCtor);
10004
+ if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH new ${owner} -> ${classExprCtor}`);
10005
+ }
9024
10006
  // ⟨R103⟩ A WRITABLE SLOT IS AN INCOMPLETE CANDIDATE SET — see `openCallSlot`, and see the class-
9025
10007
  // override fan-out below, which is the authority this converges on rather than a second rule: it
9026
10008
  // edges to every candidate it CAN name and adds `Unknown` when the set it enumerated is not
@@ -9040,8 +10022,8 @@ function visitCalls(node) {
9040
10022
  if (!decl) {
9041
10023
  // `new C()` on a class with an IMPLICIT constructor resolves to no declaration — edge to
9042
10024
  // the class's (synthesized) ctor unit via the class identifier before concluding Unknown.
9043
- let edged = false, externalClass = false;
9044
- if (ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) {
10025
+ let edged = !!classExprCtor, externalClass = false; // ⟨R1016⟩
10026
+ if (!edged && ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) {
9045
10027
  const cd = realDecl(checker.getSymbolAtLocation(node.expression));
9046
10028
  const t = cd && nodeName.get(cd);
9047
10029
  if (t) { rec.edges.add(t); edged = true; }
@@ -9143,8 +10125,95 @@ function visitCalls(node) {
9143
10125
  // never a fabrication. (A LOCAL callee keeps its precise callback-flow below.)
9144
10126
  const calleeName = ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text
9145
10127
  : ts.isIdentifier(node.expression) ? node.expression.text : null;
9146
- if (mod !== "<local>" && calleeName && HOF_INVOKERS.has(calleeName)) {
9147
- node.arguments?.forEach((a0, argIdx) => {
10128
+ // ⟨SOUNDNESS R803⟩ a LISTED name keeps its position map; an UNLISTED non-local callee is admitted at the
10129
+ // positions its signature POSITIVELY declares callable, unless denylisted. See `HOF_REMOVAL_NAMES`.
10130
+ const hofListed = !!calleeName && HOF_INVOKERS.has(calleeName);
10131
+ const hofName = calleeName ?? decl.name?.getText?.() ?? null;
10132
+ const hofOpenAt = (!hofListed && mod !== "<local>" && node.arguments?.length
10133
+ && !hofCalleeNeverInvokes(decl, hofName))
10134
+ ? (i) => calleeParamIsCallable(node, i) === true
10135
+ && !(declIsReviewedLib(decl) && calleeParamIsGenericSlot(node, i)) : null;
10136
+ // ⟨SOUNDNESS R1031⟩ THE SAME RULE ONE LEVEL DOWN: a function reference handed inside an OBJECT
10137
+ // LITERAL. rxjs `of(1).subscribe({ next: h })`, `{ complete: h }`, `{ error: h }`, `{ next }` and
10138
+ // `{ next: o.m.bind(o) }` were ABSENT with `deny Fs <caller>` exit 0 while the bare `subscribe(h)`
10139
+ // read `['Fs']` (EXECUTED: every handler that the observable fires wrote its file). R803 admits a
10140
+ // POSITION its signature declares callable; the observer object's MEMBERS are positions too, and
10141
+ // the signature declares them just as positively — `Partial<Observer<T>>`'s `next` is
10142
+ // `((value: T) => void) | undefined`. So each property whose value is a reference, at a member the
10143
+ // parameter's (non-callable) type arms declare callable, is handed to the SAME per-argument arm as
10144
+ // a bare reference — one implementation of "a reference the callee may invoke", not a second copy.
10145
+ // Inline arrows and methods in the literal were never dropped (they are structural units, charged
10146
+ // lexically). Same admission as R803's open path: an unlisted non-local callee off the denylist; an
10147
+ // `any`/`unknown`-typed parameter or member admits nothing (the R803 residual, unchanged).
10148
+ const objArgRefs = (argIdx, lit, depth = 0) => {
10149
+ if (depth > 2) return [];
10150
+ // The parameter's DECLARED type, not the call's instantiation of it: `Object.assign(p, { unsubscribe })`
10151
+ // instantiates `U` as `{ unsubscribe: () => void }`, which reads callable only because the CALL made it
10152
+ // so — the signature declares nothing about invoking it (measured: trpc's `Unpromise.then` charged
10153
+ // `callback:unsubscribe` until this read the declaration). A generic slot names no members, so it
10154
+ // admits nothing; `Partial<Observer<T>>` declares `next` callable whatever `T` is.
10155
+ let sd; try { sd = checker.getResolvedSignature?.(node)?.declaration; } catch { sd = null; }
10156
+ const dps = sd?.parameters;
10157
+ if (!dps?.length) return [];
10158
+ const dp = dps[Math.min(argIdx, dps.length - 1)];
10159
+ if (argIdx >= dps.length && !dp.dotDotDotToken) return [];
10160
+ if (!dp.type) return [];
10161
+ let pt; try { pt = checker.getTypeFromTypeNode(dp.type); } catch { pt = null; }
10162
+ if (dp.dotDotDotToken && pt) pt = checker.getIndexTypeOfType?.(pt, ts.IndexKind.Number) ?? null; // `...xs: T[]` → T
10163
+ return objLitRefsFor(pt, lit, depth);
10164
+ };
10165
+ const callableT = (t) => !!t && (t.getCallSignatures?.().length > 0 || (t.types ?? []).some((u) => u.getCallSignatures?.().length > 0));
10166
+ const objLitRefsFor = (pt, lit, depth) => {
10167
+ if (!pt || (pt.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) return [];
10168
+ const arms = (pt.types ?? [pt]).filter((u) => !(u.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Any | ts.TypeFlags.Unknown))
10169
+ && !(u.getCallSignatures?.().length > 0));
10170
+ const out = [];
10171
+ for (const pr of lit.properties) {
10172
+ const nm = pr.name && (ts.isIdentifier(pr.name) || ts.isStringLiteralLike(pr.name)) ? pr.name.text : null;
10173
+ if (!nm) continue;
10174
+ const memberTypes = arms.map((u) => { const ms = u.getProperty?.(nm); return ms ? checker.getTypeOfSymbolAtLocation(ms, lit) : null; }).filter(Boolean);
10175
+ if (ts.isShorthandPropertyAssignment(pr)) {
10176
+ if (memberTypes.some(callableT)) out.push({ shorthand: pr });
10177
+ continue;
10178
+ }
10179
+ if (!ts.isPropertyAssignment(pr)) continue; // a method / accessor is a structural unit already
10180
+ const v = unwrapArgExpr(pr.initializer);
10181
+ if (ts.isObjectLiteralExpression(v)) { for (const mt of memberTypes) out.push(...objLitRefsFor(mt, v, depth + 1)); continue; }
10182
+ if (memberTypes.some(callableT)) out.push({ ref: pr.initializer });
10183
+ }
10184
+ return out;
10185
+ };
10186
+ if (mod !== "<local>" && (hofListed || hofOpenAt)) {
10187
+ const hofArgArm = (a0, argIdx) => {
10188
+ // ⟨R1031⟩ an object literal at an admitted callee: its referenced members are the arguments.
10189
+ if (!hofListed) {
10190
+ const lit = unwrapArgExpr(a0);
10191
+ if (lit && ts.isObjectLiteralExpression(lit)) {
10192
+ // A property DESCRIPTOR installs its `get`/`set` and invokes neither; the access sites are
10193
+ // edged to the installed accessor by `definePropAccessors`. Charging the definer would be the
10194
+ // over-charge R803's generic-slot denylist exists for, one level down.
10195
+ if (declIsReviewedLib(decl) && OBJ_ARG_INSTALLS.has(hofOwnerMember(decl) ?? "")) return;
10196
+ for (const r of objArgRefs(argIdx, lit)) {
10197
+ if (r.ref) { HOF_REACH?.("objref", `${hofName}`); hofArgArm(r.ref, -1 - argIdx); continue; }
10198
+ // `{ next }` — the shorthand's VALUE symbol, followed one alias hop to what it holds.
10199
+ const vs = checker.getShorthandAssignmentValueSymbol(r.shorthand);
10200
+ const vd = vs?.valueDeclaration;
10201
+ HOF_REACH?.("objref", `${hofName}`);
10202
+ if (vd && nodeName.get(vd)) rec.edges.add(nodeName.get(vd));
10203
+ else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && !ts.isArrowFunction(vd.initializer)
10204
+ && !ts.isFunctionExpression(vd.initializer)) hofArgArm(vd.initializer, -1 - argIdx);
10205
+ else if (vd && ts.isVariableDeclaration(vd) && vd.initializer && nodeName.get(vd.initializer)) rec.edges.add(nodeName.get(vd.initializer));
10206
+ else if (!vd || ts.isParameter(vd) || ts.isVariableDeclaration(vd) || ts.isBindingElement(vd)) {
10207
+ rec.direct.add("Unknown"); rec.why.add(`callback:${r.shorthand.name.text.slice(0, 40)}`);
10208
+ }
10209
+ }
10210
+ return;
10211
+ }
10212
+ }
10213
+ // ⟨R1031⟩ a member reference re-entering the arm carries a NEGATIVE index: it is admitted by its
10214
+ // member's declared type above, and no position map or signature lookup applies to it.
10215
+ const memberRef = argIdx < 0;
10216
+ if (!memberRef && !hofListed && !hofOpenAt(argIdx)) return;
9148
10217
  // R947 — a TYPE-ONLY wrapper is the same reference: `xs.forEach(fs.unlinkSync as any)`
9149
10218
  // was dropped by the id/property-access gate below (no edge, no κ, no disclosure) while the bare
9150
10219
  // spelling was not. `as`/`!`/`satisfies`/parentheses change no runtime value.
@@ -9159,7 +10228,7 @@ function visitCalls(node) {
9159
10228
  // the same over-charge the by-reference arm below was fixed for and this arm was not.
9160
10229
  // …but it must be the NEGATIVE test, not `!hofInvokesArg`: an unresolved or loosely-typed
9161
10230
  // callee signature is not a licence to drop a bound callback. See `hofArgIsNeverCallback`.
9162
- if (hofArgIsNeverCallback(calleeName, argIdx, node)) return;
10231
+ if (hofListed && hofArgIsNeverCallback(calleeName, argIdx, node)) return;
9163
10232
  const bound = unwrapBind(a);
9164
10233
  if (bound) {
9165
10234
  const bref = bound.ref;
@@ -9212,7 +10281,8 @@ function visitCalls(node) {
9212
10281
  if (!ts.isIdentifier(a) && !ts.isPropertyAccessExpression(a)) return;
9213
10282
  const d2 = realDecl(checker.getSymbolAtLocation(a));
9214
10283
  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
10284
+ if (t) { if (!hofListed) HOF_REACH?.("edge", `${hofName}->${t}`);
10285
+ rec.edges.add(t); return; } // resolvable named/local callback — keep its analyzed effect
9216
10286
  // A DEPENDENCY function passed BY REFERENCE — `xs.forEach(depWrite)`, `setTimeout(dep.tick, 0)`.
9217
10287
  // The invoking HOF calls it, so its effects are reachable here, and the dependency's report
9218
10288
  // holds them under `<pkg>#depWrite`. Guard (2) below reasons that a ref resolving to a concrete
@@ -9238,7 +10308,7 @@ function visitCalls(node) {
9238
10308
  // through the κ/invisible channel, so blanket-Unknown here would over-disclose them.
9239
10309
  // (3) CALLABILITY — `argIsCallable` (has a call signature, or `any`/`unknown`/unconstrained
9240
10310
  // generic that COULD hold a function).
9241
- if (!hofInvokesArg(calleeName, argIdx, node)) return;
10311
+ if (hofListed && !hofInvokesArg(calleeName, argIdx, node)) return;
9242
10312
  // ⟨SOUNDNESS R558⟩ …and BEFORE the by-reference dependency charge below, without returning:
9243
10313
  // a FOREIGN interface member named as a value needs BOTH the visible-implementor join and
9244
10314
  // the `invisible`/ledger disclosure `chargeExternalDecl` already gives it, and the two
@@ -9253,6 +10323,7 @@ function visitCalls(node) {
9253
10323
  // dep VALUE with a call signature, passes. Guard (1) exists for exactly this shape and its own
9254
10324
  // comment names the case (`path.reduce(fn, obj)`); the new arm simply ran before it.
9255
10325
  if (d2 && !declIsLocal(d2) && argIsCallable(a)) {
10326
+ if (!hofListed) HOF_REACH?.("dep", `${hofName}->${a.getText().slice(0, 40)}`);
9256
10327
  // R947 — A κ-CLASSIFIED BUILTIN PASSED BY REFERENCE. `chargeExternalDecl` is the
9257
10328
  // dependency funnel and never asks κ, and `disclosureTail` treats a κ-KNOWN member as covered, so
9258
10329
  // `xs.forEach(fs.unlinkSync)`, `setTimeout(fs.unlinkSync, 0, p)`, `.then(cp.execSync)` and
@@ -9275,11 +10346,24 @@ function visitCalls(node) {
9275
10346
  const holderIsProjectValue = d2 && (ts.isParameter(d2) || ts.isVariableDeclaration(d2) || ts.isBindingElement(d2))
9276
10347
  && projectFiles.has(path.resolve(d2.getSourceFile().fileName));
9277
10348
  if (!(holderIsProjectValue || !d2)) return;
10349
+ // ⟨SOUNDNESS R803⟩ a newly admitted callee handed its OWN caller's parameter (`reg(e, h) { e.on("y", h) }`)
10350
+ // is the callback flow's question, not an opaque value: the parameter is INVOKED, so pass 2b resolves it
10351
+ // to what every visible call site passed (an edge — `deny Fs` on the caller of `reg` fires) and keeps the
10352
+ // honest `callback:param#i` where a call site is opaque or none is visible. The listed names keep their
10353
+ // older `callback:<name>` disclosure — unchanged here so this fix removes nothing it did not add.
10354
+ if (!hofListed && d2 && ts.isParameter(d2) && d2.parent && nodeName.get(d2.parent) && argIsCallable(a)) {
10355
+ const ownerUnit = nodeName.get(d2.parent);
10356
+ HOF_REACH?.("param", `${hofName}->${a.getText().slice(0, 40)}`);
10357
+ (paramInvokes.get(ownerUnit) ?? paramInvokes.set(ownerUnit, new Set()).get(ownerUnit)).add(d2.parent.parameters.indexOf(d2));
10358
+ return;
10359
+ }
9278
10360
  if (argIsCallable(a)) {
10361
+ if (!hofListed) HOF_REACH?.("opaque", `${hofName}->${a.getText().slice(0, 40)}`);
9279
10362
  rec.direct.add("Unknown");
9280
10363
  rec.why.add(`callback:${a.getText().replace(/\s+/g, "").slice(0, 40)}`); // opaque callable invoked by a sync HOF — canonical `callback:`
9281
10364
  }
9282
- });
10365
+ };
10366
+ node.arguments?.forEach(hofArgArm);
9283
10367
  }
9284
10368
  // `fn.call(thisArg, …)` / `fn.apply(thisArg, args)` INVOKE the receiver function reference, and
9285
10369
  // `Reflect.apply(fn, …)` / `Reflect.construct(Ctor, …)` invoke their FIRST ARGUMENT. The resolved
@@ -9317,6 +10401,11 @@ function visitCalls(node) {
9317
10401
  if ((m === "call" || m === "apply") && recvText !== "Reflect") invokedRef = recv;
9318
10402
  else if (recvText === "Reflect" && (m === "apply" || m === "construct"))
9319
10403
  invokedRef = (node.arguments ?? [])[0] ?? null;
10404
+ // ⟨SOUNDNESS R780⟩ `(k.m as F).call(k)`, `(f satisfies F).call(t)` and `Reflect.apply(f as F, …)` invoke what
10405
+ // the bare spelling invokes. A single wrapped reference is not a CHOICE, so R955's leaves (which do unwrap)
10406
+ // returned null and the id/property gate below saw a ParenthesizedExpression: no edge, no `Unknown`, the
10407
+ // caller ABSENT (EXECUTED, `fx/r780`).
10408
+ invokedRef = unwrapArgExpr(invokedRef);
9320
10409
  // ⟨SOUNDNESS R587⟩ …and the ELEMENT-ACCESS spelling of the invoked reference, on the same
9321
10410
  // grounds as the HOF-ref arm above: `i["roll"].call(null, n)` invokes exactly what
9322
10411
  // `i.roll.call(null, n)` invokes. Measured ABSENT at `6a639e6` in every cell
@@ -9430,8 +10519,7 @@ function visitCalls(node) {
9430
10519
  const recvExpr = (ts.isPropertyAccessExpression(node.expression)
9431
10520
  || ts.isElementAccessExpression(node.expression)) ? node.expression.expression : null;
9432
10521
  const rootClass = localReceiverClass(recvExpr);
9433
- const direct = rootClass ? allOverrides.filter((om) =>
9434
- ts.isClassDeclaration(om.parent) && classInSubtree(om.parent, rootClass)) : allOverrides;
10522
+ const direct = rootClass ? allOverrides.filter((om) => classInSubtree(overrideOwner(om), rootClass)) : allOverrides;
9435
10523
  const overrides = isSuperReceiver(recvExpr) ? [] : memberDispatchBodies(decl, rootClass);
9436
10524
  if (isSuperReceiver(recvExpr)) probeR871("super-call", decl, direct.length);
9437
10525
  else if (direct.length <= CHA_FANOUT_LIMIT) probeR871("class", decl, overrides.length - direct.length);
@@ -9760,6 +10848,22 @@ function visitCalls(node) {
9760
10848
  rec.direct.add("Unknown");
9761
10849
  rec.why.add(`native:Storage.${name}`);
9762
10850
  }
10851
+ // SOUNDNESS R241 — THE REST OF THE BROWSER'S PERSISTENT STORES AND NAVIGATION, which the comment
10852
+ // above named as "untouched and still pure" (the documented-limitation shape: writing it down is
10853
+ // what stopped it being measured). IndexedDB, the Cache API and the Origin Private File System
10854
+ // persist across sessions exactly as `Storage` does, and `location.assign`/`replace`/`reload`
10855
+ // navigate (a request leaves the page). All six of the row's spellings read ABSENT under
10856
+ // `lib: ["ES2022","DOM"]`, while `localStorage.setItem` on the same tree read `Unknown[native:…]`.
10857
+ // Same answer as `Storage`, for the same reason: the WHOLE interface (the denylist direction — an
10858
+ // unlisted member would be silent), `Unknown` because the backing store / navigation target is not
10859
+ // modelled, keyed on the RESOLVED DECLARATION'S PARENT so a project's own `class Cache` resolves
10860
+ // `<local>` and never reaches this arm. Reach on the (Node/server) roster is zero by construction;
10861
+ // the price is paid only by code that already touches these hosts.
10862
+ if (BROWSER_STORE_IFACES.has(parent)
10863
+ || (parent === "Location" && (name === "assign" || name === "replace" || name === "reload"))) {
10864
+ rec.direct.add("Unknown");
10865
+ rec.why.add(`native:${parent}.${name}`);
10866
+ }
9763
10867
  // `new EventSource(url)` / `new WebSocket(url)`: the constructor is declared on an anonymous
9764
10868
  // `declare var` object type (symbol `__type`, no usable parent name), but reaching the es-lib
9765
10869
  // branch already proves the ctor resolved to lib.dom (not a project class shadowing the name),
@@ -9997,6 +11101,22 @@ function visitCalls(node) {
9997
11101
  && (ts.isPropertyAccessExpression(node.expression) || ts.isElementAccessExpression(node.expression))
9998
11102
  && receiverIsProvenNonNetworkStream(node.expression.expression))
9999
11103
  { eff = null; effSuppressed = true; }
11104
+ // ⟨SOUNDNESS R966⟩ A FRAMEWORK'S OWN `listen` IS AN ACCEPT. `NET_ACCEPTING` is asked only of a call κ
11105
+ // classified `Net`, and κ classifies node's servers — so express `app.listen(p)` (declared in
11106
+ // express-serve-static-core), koa `app.listen(p)` and fastify `f.listen({port})` reached neither the effect
11107
+ // nor the accept mark: beside `net.connect(80, "ok.example")`, `allow Net in <fn> ok.example` exited 0
11108
+ // over a server answering anyone (EXECUTED: a client got the handler's reply). An inclusion list decided a
11109
+ // fail-closed question (R781's lesson), so this is the fail-closed reading: a DEPENDENCY member named
11110
+ // `listen` that κ did not answer is a network accept — `Net`, and `netAccepting` marks it `incomplete`.
11111
+ // The failure direction is an over-charge on a non-network `listen`; the one family known to be that is
11112
+ // denylisted by package (a JSON-RPC `connection.listen()` starts reading a transport whose OWN
11113
+ // construction decides whether it is a socket). Measured reach on the pinned roster: 1 unclassified
11114
+ // dependency `listen` (nest's FastifyAdapter), 0 non-network. Not seen: hapi `server.start()`
11115
+ // (`start` is p-queue's and repl's too), Bun.serve/Deno.serve (no types in any roster tree).
11116
+ if (!eff && !effSuppressed && member === "listen" && dependencyListenIsAccept(mod, decl)) {
11117
+ eff = "Net";
11118
+ if (process.env.CANDOR_R966_REACH) console.error(`R966-REACH ${mod}.listen`);
11119
+ }
10000
11120
  if (eff) {
10001
11121
  rec.direct.add(eff);
10002
11122
  // SPEC §2 `fs` — refine an Fs we just PROVED with the direction its verb implies. DIRECT only
@@ -10327,7 +11447,16 @@ function visitCalls(node) {
10327
11447
  // `const [,,x] = process.argv`, or handing it to a parser all reach the same process-startup state.
10328
11448
  // Marked on the expression itself so every idiom counts without enumerating them.
10329
11449
  if (isProcessArgvExpr(node)) markEnv();
11450
+ // ⟨R934⟩ a DESTRUCTURED parameter that every visible call site hands the environment reads its keys
11451
+ // as the frame is entered — the destructuring IS the read, in this function's frame.
11452
+ if (ts.isParameter(node) && ts.isObjectBindingPattern(node.name) && envAliasSymbols.has(node)) { markEnv(); ENVLOC_REACH?.("param-pattern env"); }
10330
11453
  const kind = envRefKind(node);
11454
+ if (kind && ENVLOC_REACH && !envConsumerInert(node)) {
11455
+ const vd = ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.valueDeclaration : null;
11456
+ const where = `${path.relative(process.cwd(), node.getSourceFile().fileName)}:${node.getStart() >= 0 ? node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1 : 0}`;
11457
+ if (vd && ts.isParameter(vd)) ENVLOC_REACH(`param ${kind} ${where}`);
11458
+ else if (!ts.isIdentifier(node) && !isProcessEnvExpr(node)) ENVLOC_REACH(`prop ${kind} ${where}`);
11459
+ }
10331
11460
  if (kind && !envConsumerInert(node)) {
10332
11461
  if (kind === "env") markEnv();
10333
11462
  else {
@@ -10368,6 +11497,24 @@ function visitCalls(node) {
10368
11497
  && isWebStorageExpr(node.expression)) {
10369
11498
  markStore(memberLabel(node));
10370
11499
  }
11500
+ // SOUNDNESS R241 — the two PROPERTY spellings of the same host boundary, which no call arm sees: the
11501
+ // cookie jar (`document.cookie`, read OR written — it is a persistent store, `Storage`'s posture) and a
11502
+ // WRITE to any `Location` property (`location.href = u` navigates; so do `pathname`/`search`/`hash`).
11503
+ // lib.dom declares both as DATA properties, so the accessor arm finds nothing either. Keyed on the
11504
+ // resolved declaration living in the TypeScript lib and on its declaring interface, never on text.
11505
+ if (ts.isPropertyAccessExpression(node)) {
11506
+ const hsym = checker.getSymbolAtLocation(node.name);
11507
+ const hdecl = (hsym?.declarations ?? []).find((d) => /typescript\/lib\/lib\..*\.d\.ts$/.test(path.resolve(d.getSourceFile().fileName).replace(/\\/g, "/")));
11508
+ const iface = hdecl?.parent && ts.isInterfaceDeclaration(hdecl.parent) ? hdecl.parent.name.text : null;
11509
+ const pn = node.name.text;
11510
+ const hp = node.parent;
11511
+ const written = (hp && ts.isBinaryExpression(hp) && hp.left === node && envIsAssignOp(hp.operatorToken.kind))
11512
+ || isDestructuringAssignTarget(node);
11513
+ if ((iface === "Document" && pn === "cookie") || (iface === "Location" && written)) {
11514
+ const owner = enclosing(node);
11515
+ if (owner) { const r = fns.get(owner); r.direct.add("Unknown"); r.why.add(`native:${iface}.${pn}`); }
11516
+ }
11517
+ }
10371
11518
  else if (ts.isVariableDeclaration(node) && node.name && ts.isObjectBindingPattern(node.name)
10372
11519
  && node.initializer && isWebStorageExpr(node.initializer)) {
10373
11520
  markStore("<destructure>");
@@ -10788,6 +11935,36 @@ function visitCalls(node) {
10788
11935
  && p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
10789
11936
  const recordKind = (kind) => {
10790
11937
  const hits = accessorsAt(node, kind);
11938
+ // ⟨R246/R1039⟩ THE RESIDUE: a WRITE (R246) or READ (R1039) through a receiver whose static type is
11939
+ // `any`/`unknown` and whose value neither the assertion look-through nor the parameter provenance could
11940
+ // name. Recorded here; whether it is disclosed is decided after the fixpoint, when it is known which
11941
+ // project accessors of that kind have effects.
11942
+ if (!(hits && hits.length)) {
11943
+ let rt1; try { rt1 = checker.getTypeAtLocation(node.expression); } catch { rt1 = null; }
11944
+ if (anyishType(rt1) && !receiverValueTypes(node.expression).length) {
11945
+ const owner1 = enclosing(node);
11946
+ const nm1 = ts.isPropertyAccessExpression(node) ? node.name.text
11947
+ : (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
11948
+ if (owner1) anyRecvSetResidue.push({ owner: owner1, name: nm1, kind });
11949
+ }
11950
+ }
11951
+ if (R246_REACH) {
11952
+ let rt0; try { rt0 = checker.getTypeAtLocation(node.expression); } catch { rt0 = null; }
11953
+ if (anyishType(rt0) || peelAssertions(node.expression) !== node.expression) {
11954
+ const vts = receiverValueTypes(node.expression);
11955
+ const nm = ts.isPropertyAccessExpression(node) ? node.name.text
11956
+ : (node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null);
11957
+ if (hits && hits.length && vts.length) R246_REACH(`resolved ${kind}`);
11958
+ else if (anyishType(rt0) && !vts.length) {
11959
+ projAccessorNames ??= (() => { const m = { get: new Set(), set: new Set() };
11960
+ const v = (n) => { if ((ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n)) && n.name && !ts.isComputedPropertyName(n.name))
11961
+ (ts.isGetAccessorDeclaration(n) ? m.get : m.set).add(n.name.getText()); ts.forEachChild(n, v); };
11962
+ for (const sf of sources) v(sf); return m; })();
11963
+ const could = nm == null ? projAccessorNames[kind].size > 0 : projAccessorNames[kind].has(nm);
11964
+ R246_REACH(`residue ${kind} ${nm == null ? "dyn" : "named"} ${could ? "could-hit-project-accessor" : "no-project-accessor"}`);
11965
+ }
11966
+ }
11967
+ }
10791
11968
  if (hits && hits.length) {
10792
11969
  const owner = enclosing(node);
10793
11970
  if (!owner) return;
@@ -10864,7 +12041,10 @@ function visitCalls(node) {
10864
12041
  // the four symbol rows below); what changed is that the other spelling now asks the same code.
10865
12042
  const keyT = ts.isElementAccessExpression(node) && node.argumentExpression
10866
12043
  ? checker.getTypeAtLocation(node.argumentExpression) : null;
10867
- for (const prop of (rt?.getProperties?.() ?? [])) {
12044
+ // R246 — the VALUE's types too: `(s as any)[k] = v`, or `o[k] = v` with `o: any` fed an `S`.
12045
+ const rprops = [...(rt?.getProperties?.() ?? [])];
12046
+ for (const vt of receiverValueTypes(node.expression)) rprops.push(...(vt.getProperties?.() ?? []));
12047
+ for (const prop of rprops) {
10868
12048
  if (!accessorsFromSym(prop, kind).length) continue;
10869
12049
  if (!keyCouldNameAccessor(prop, keyT)) continue;
10870
12050
  const owner = enclosing(node);
@@ -11282,17 +12462,39 @@ for (const sf of sources) visitCalls(sf);
11282
12462
  })(cls);
11283
12463
  for (const dec of decorators) {
11284
12464
  add(localBodiedUnit(dec), "deco"); // the application
11285
- let e = dec.expression;
11286
- while (ts.isParenthesizedExpression(e)) e = e.expression;
12465
+ const e = unwrapArgExpr(dec.expression); // R780
11287
12466
  if (ts.isCallExpression(e)) {
11288
12467
  add(localBodiedUnit(e), "deco"); // the factory call
11289
12468
  add(`${moduleOf(e.getSourceFile())}.${decoratorArgLocal(e)}`, "arg"); // ⟨R944⟩ the ONE spelling
11290
12469
  }
11291
12470
  }
11292
12471
  for (const m of cls.members ?? [])
11293
- if (ts.isClassStaticBlockDeclaration(m)) add(staticBlockQual(m), "static");
12472
+ if (ts.isClassStaticBlockDeclaration(m)
12473
+ || (ts.isPropertyDeclaration(m) && m.initializer && (ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)))
12474
+ add(staticBlockQual(m), "static"); // R815: a static field initialiser shares the block's unit
12475
+ // R815: the `extends <expr>` unit — run by the definer (here) and reached by every construction (`super`).
12476
+ const ctorQ = nodeName.get(cls);
12477
+ const herQ = ctorQ && ctorQ.endsWith(".constructor") ? ctorQ.replace(/\.constructor$/, ".<heritage>") : null;
12478
+ // Wired only when the expression attributed something: a bare `extends Base` mints the unit (a reference was
12479
+ // attributed) and carries nothing, and an edge to an empty unit is a `calls` entry on every subclass for no answer.
12480
+ const herR = herQ && fns.get(herQ);
12481
+ if (herR && ["direct", "edges", "why", "blind", "hosts", "paths", "cmds", "incomplete", "dispatch", "tables"]
12482
+ .some((k) => herR[k]?.size)) { add(herQ, "heritage"); fns.get(ctorQ)?.edges.add(herQ); }
12483
+ // ⟨R1016⟩ the containment edge to a class expression's constructor, unless its value cannot escape a `new`.
12484
+ // Asked from the class itself rather than its parent, so a `static X = class {…}` lands on `<static-init>`.
12485
+ const ceq = ts.isClassExpression(cls) ? classExprCtorQual.get(cls) : null;
12486
+ if (ceq && classExprEscapes(cls)) {
12487
+ const cfrom = enclosing(cls);
12488
+ const crec = cfrom && cfrom !== ceq ? fns.get(cfrom) : null;
12489
+ if (crec && !crec.edges.has(ceq)) { crec.edges.add(ceq); hit("classexpr"); }
12490
+ } else if (ceq && process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH contained-dropped ${ceq}`);
11294
12491
  if (!targets.length) continue;
11295
- const from = enclosing(cls.parent);
12492
+ // ⟨SOUNDNESS R1060⟩ a class EXPRESSION is asked from itself, as the R1016 edge above is: climbing from its PARENT
12493
+ // starts at a `static X = class {…}` property with no child in hand, so R815's static-initialiser rule never
12494
+ // matched and the inner class's definition-time work was wired from the OUTER class's constructor — `new HB()`
12495
+ // charged for a `static {}` block that runs once, at import (EXECUTED), and the module that runs it silent.
12496
+ // A declaration is still asked from its parent: from itself, `nodeName` would answer its own constructor.
12497
+ const from = ts.isClassExpression(cls) ? enclosing(cls) : enclosing(cls.parent);
11296
12498
  const rec = from && fns.get(from);
11297
12499
  if (!rec) continue;
11298
12500
  for (const [q, k] of targets) if (q !== from && !rec.edges.has(q)) { rec.edges.add(q); hit(k); }
@@ -11797,6 +12999,52 @@ const inferred = new Map([...fns.keys()].map((k) => [k, new Set(fns.get(k).direc
11797
12999
  if (!queued.has(c)) { queued.add(c); queue.push(c); }
11798
13000
  }
11799
13001
  }
13002
+ // ⟨SOUNDNESS R246⟩ THE HEDGE FOR WHAT RESOLUTION CANNOT REACH. A write `o.name = v` / `o[k] = v` through a
13003
+ // receiver typed `any`/`unknown` whose value neither an assertion nor a visible call site names — an escaped
13004
+ // function's parameter, an exported one with no visible caller, an `any` field or return — can still land
13005
+ // on a project SETTER (EXECUTED: `[new Sx()].forEach(w)` with `w(o: any) { o.token = v }` ran Sx's setter, and
13006
+ // `w` was ABSENT, `deny Fs`/`deny Unknown` both exit 0). Which object it is cannot be known, so this
13007
+ // DISCLOSES rather than charges: `Unknown[reflect:accessor:any-receiver]` on the writer, and only when the
13008
+ // project declares a setter that could be the one — the SAME name (or any string-named setter for an
13009
+ // unpinnable key) — AND that setter's body has an effect. A setter-free or pure-setter project pays nothing.
13010
+ // Priced on the 28-entry roster (see the commit for the measured count — a handful of units, <0.1%).
13011
+ // SOUNDNESS R1039 — THE READ SIDE, SAME RULE: `o.name` / `o[k]` through such a receiver can run a project
13012
+ // GETTER (EXECUTED: an escaped `r(o: any) { return o.token }` handed a class whose `get token()` writes a
13013
+ // file — `r` ABSENT, `deny Unknown`/`deny Fs Unknown` exit 0 on v0.40.0). Same admission: a getter WITH A
13014
+ // BODY and an EFFECT that the read could name (the same name, or any string-named one for an unpinnable key).
13015
+ if (anyRecvSetResidue.length) {
13016
+ const accUnits = { get: new Map(), set: new Map() }; // kind -> name -> [unit]
13017
+ const visitS = (n) => {
13018
+ // An accessor WITH A BODY: a body-less declaration (a project typings stub) is already its own `native:`
13019
+ // disclosure, and arming every `any` write on it measured as pure noise (typeorm's bson `ObjectId.set id`).
13020
+ if ((ts.isSetAccessorDeclaration(n) || ts.isGetAccessorDeclaration(n)) && n.body && n.name && !ts.isComputedPropertyName(n.name)) {
13021
+ const u = nodeName.get(n);
13022
+ const m = ts.isSetAccessorDeclaration(n) ? accUnits.set : accUnits.get;
13023
+ if (u) { const k = n.name.getText(); (m.get(k) ?? m.set(k, []).get(k)).push(u); }
13024
+ }
13025
+ ts.forEachChild(n, visitS);
13026
+ };
13027
+ for (const sf of sources) visitS(sf);
13028
+ const effectful = (u) => (inferred.get(u)?.size ?? 0) > 0;
13029
+ const anyEffectful = { get: [...accUnits.get.values()].flat().some(effectful), set: [...accUnits.set.values()].flat().some(effectful) };
13030
+ const hedged = new Set();
13031
+ for (const { owner, name, kind } of anyRecvSetResidue) {
13032
+ const hit = name == null ? anyEffectful[kind] : (accUnits[kind].get(name) ?? []).some(effectful);
13033
+ if (!hit || !fns.has(owner)) continue;
13034
+ const rec = fns.get(owner);
13035
+ rec.direct.add("Unknown"); rec.why.add("reflect:accessor:any-receiver");
13036
+ hedged.add(owner);
13037
+ }
13038
+ // propagate the added `Unknown` to every transitive caller (the fixpoint above has already run)
13039
+ const queue = [...hedged];
13040
+ const seenU = new Set(queue);
13041
+ for (let head = 0; head < queue.length; head++) {
13042
+ const u = queue[head];
13043
+ inferred.get(u)?.add("Unknown");
13044
+ for (const c of callersOf.get(u) ?? [])
13045
+ if (!seenU.has(c) && !inferred.get(c)?.has("Unknown")) { seenU.add(c); queue.push(c); }
13046
+ }
13047
+ }
11800
13048
  // `fsKinds` joins the propagated surfaces: kinds TRAVEL the call graph (a caller that transitively only
11801
13049
  // writes IS a writer), and the "?" poison travels with them so a caller of an undetermined-kind function
11802
13050
  // inherits the SUPPRESSION rather than a half-answer. Pinned by conformance PART 31.