candor-ts 0.40.1 → 0.40.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/package.json +6 -3
  3. package/scan.mjs +1096 -73
package/scan.mjs CHANGED
@@ -1653,11 +1653,60 @@ if (NO_SOURCES) {
1653
1653
  // types). Resolved via the module system, NOT a fixed relative path — npm HOISTS dependencies, so
1654
1654
  // in an npx/install tree @types/node sits BESIDE candor-ts, not inside it (the second probe's
1655
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
+ }
1656
1687
  if (!compilerOptions.typeRoots) {
1657
1688
  const roots = [path.join(rootDir, "node_modules", "@types")];
1658
1689
  try {
1659
1690
  const req = createRequire(path.join(ENGINE_DIR, "scan.mjs"));
1660
- 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
+ }
1661
1710
  } catch {}
1662
1711
  compilerOptions.typeRoots = roots;
1663
1712
  }
@@ -2595,6 +2644,29 @@ const unaliasGlobal = (expr) => {
2595
2644
  };
2596
2645
  const projectFiles = new Set(fileNames.map((f) => path.resolve(f)));
2597
2646
  const sources = program.getSourceFiles().filter((f) => projectFiles.has(path.resolve(f.fileName)));
2647
+ // ⟨SOUNDNESS R1074⟩ DOES A PROJECT DECLARATION SHADOW AN AMBIENT GLOBAL? Eight checks asked "is any declaration of
2648
+ // this symbol in a project file?", and that is not the question: in a JavaScript SCRIPT the checker records every
2649
+ // expando ASSIGNMENT as a declaration of the global it writes through. One `process.env['K'] = v` in a project
2650
+ // `.cjs`/`.mjs`/`.js` script gives the global `process` a declaration in that file, and every `process.env` read
2651
+ // in the WHOLE PROJECT then failed the "ambient global" test. MEASURED (EXECUTED, `fx74`): `readSecret() { return
2652
+ // process.env.SECRET }` printed the planted value and was ABSENT, `deny Env readSecret` exit 0, beside a one-line
2653
+ // script that writes a different key; the same file without the script is `['Env']`. On the pinned corpus, every
2654
+ // `process.env` read across drizzle-orm (two driver-test scripts assign `process.env['POSTGRES_URL']`).
2655
+ // A shadow is a declaration that CREATES a different runtime binding: a non-ambient variable, function, class,
2656
+ // parameter, binding element or import. An expando (an Identifier/PropertyAccess/ElementAccess/BinaryExpression
2657
+ // declaration node, or `Object.defineProperty` on the global), a `declare` statement, a `.d.ts` file and a
2658
+ // `declare global {}` block all name the global itself and shadow nothing.
2659
+ const declShadowsGlobal = (d) => {
2660
+ if (!projectFiles.has(path.resolve(d.getSourceFile().fileName)) || d.getSourceFile().isDeclarationFile) return false;
2661
+ if (!(ts.isVariableDeclaration(d) || ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d) || ts.isParameter(d)
2662
+ || ts.isBindingElement(d) || ts.isImportSpecifier(d) || ts.isImportClause(d) || ts.isNamespaceImport(d)
2663
+ || ts.isImportEqualsDeclaration(d) || ts.isEnumDeclaration(d) || ts.isModuleDeclaration(d))) return false;
2664
+ if (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) return false;
2665
+ for (let p = d.parent; p; p = p.parent)
2666
+ if (ts.isModuleDeclaration(p) && (p.flags & ts.NodeFlags.GlobalAugmentation)) return false;
2667
+ return true;
2668
+ };
2669
+ const projectShadows = (decls) => (decls ?? []).some(declShadowsGlobal);
2598
2670
 
2599
2671
  // ⟨0.21⟩ COMPLETENESS MANIFEST (Gap 2): the TARGET's own source candor could NOT analyze — a .ts that
2600
2672
  // FAILED TO PARSE (a syntax error). getSyntacticDiagnostics() reports lexer/parser failures; the source
@@ -3483,7 +3555,7 @@ function determinedUrlObject(expr, call, depth = 0) {
3483
3555
  && (expr.expression.text === "URL" || expr.expression.text === "Request")) {
3484
3556
  const decls = checker.getSymbolAtLocation(expr.expression)?.declarations ?? [];
3485
3557
  if (decls.length === 0
3486
- || decls.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) return null;
3558
+ || projectShadows(decls)) return null; // ⟨R1074⟩
3487
3559
  const args = expr.arguments ?? [];
3488
3560
  const max = expr.expression.text === "URL" ? 2 : 1;
3489
3561
  if (args.length === 0 || args.length > max) return null;
@@ -4023,6 +4095,7 @@ const fns = new Map(); // qualified name -> { direct, edges, hosts, ta
4023
4095
  const bodylessDecls = new Map(); // qual -> {node, mod, abstract, owner, member}
4024
4096
  const unlistedSeen = new Map(); // the κ-coverage ledger: unlisted npm package -> call-site count
4025
4097
  const nodeName = new WeakMap(); // declaration node -> qualified name
4098
+ const classExprCtorQual = new Map(); // ⟨R1016⟩ ClassExpression -> its constructor unit (see `mintClassExprCtor`)
4026
4099
  // ⟨scan-boundary, export-alias⟩ `module.exports = { thing: _internalImplName }` / `exports.thing =
4027
4100
  // _internalImplName` — a RENAMED re-export whose RHS NAMES an EXISTING declaration rather than being an
4028
4101
  // inline function. `localName` mints a CJS unit for `module.exports = function(){}` and
@@ -4187,6 +4260,10 @@ const classDescendants = new Map();// base ClassDeclaration -> transitive LOCAL
4187
4260
  // R954 — the conformer registries (filled by the conversion pass after `walkStructural`).
4188
4261
  const classConformers = new Map(); // local ClassDeclaration -> conformer nodes (local class, literal, dep class)
4189
4262
  const depConformers = new Map(); // InterfaceDeclaration (local or foreign) -> dependency ClassDeclarations
4263
+ // ⟨R1061⟩ local classes a DI provider substitutes with a value this scan cannot name: a dispatch on one discloses.
4264
+ const openSubstitutedClasses = new Set();
4265
+ const tokenProviders = new Map(); // ⟨R1061⟩ non-class provider token key -> [{ lit, prop }] (see `injectTokenKey`)
4266
+ const tokenDeclIds = new Map();
4190
4267
  const ifaceFlows = new Map(); // local InterfaceDeclaration J -> Set of targets a J-typed value is converted to
4191
4268
  const isProjectNode = (d) => !!d && projectFiles.has(path.resolve(d.getSourceFile().fileName));
4192
4269
  const conversionAddedImpls = new Map(); // InterfaceDeclaration -> Set of implementors the conversion pass added
@@ -4563,6 +4640,7 @@ const STABLE_KEY_SHAPES = {
4563
4640
  callable: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
4564
4641
  decorator: new Set([ts.SyntaxKind.ArrowFunction, ts.SyntaxKind.FunctionExpression]),
4565
4642
  "decorator-arg": new Set([ts.SyntaxKind.CallExpression]),
4643
+ classexpr: new Set([ts.SyntaxKind.ClassExpression]), // ⟨R1016⟩ a class expression's constructor unit
4566
4644
  computed: null, // a descriptor member of any shape — counted over EVERY node under the anchor
4567
4645
  };
4568
4646
  const stableKeySanitise = (s) => s.replace(/[^A-Za-z0-9_$]/g, "_");
@@ -4890,6 +4968,42 @@ function descriptorAliasRef(descProp) {
4890
4968
  // True when `node` is lexically inside a FUNCTION body — so its bare local name can collide with a
4891
4969
  // module-level or sibling-scope unit of the same name (see the qual disambiguation below). A namespace/
4892
4970
  // module block and the source file are NOT function scopes (their members are top-level units).
4971
+ // ⟨SOUNDNESS R1067/R1074⟩ THE KEYS OF A CLASS'S SYNTHESIZED UNITS, spelled once. A named function's, method's and
4972
+ // explicit constructor's qual carries `#line:col` when FUNCTION-SCOPED (above), because two scopes may declare the
4973
+ // same name. The units a CLASS mints for itself — its constructor when implicit (instance field initialisers), its
4974
+ // `<static-init>` and its `<heritage>` — were keyed on the bare class name, so two function-scoped `class K` in one
4975
+ // module shared one unit, and every anonymous class EXPRESSION's static block shared `<anonymous>.<static-init>`.
4976
+ // MEASURED (EXECUTED, `fx67`): six pure definers charged `Fs` from a sibling's block or field (`deny Fs` exit 1 over
4977
+ // code that writes nothing), and — the silent half — a function-scoped class with an EXPLICIT constructor kept its
4978
+ // field initialisers on the bare `K.constructor` unit while `new K()` resolved to the positioned explicit one, so
4979
+ // the constructing function was ABSENT and `deny Fs` exited 0 over a write that ran.
4980
+ // The class's own `.constructor` unit IS its explicit implementation's when there is one (the top-level spelling
4981
+ // already coincided); otherwise it is positioned at the class like any function-scoped unit.
4982
+ function classCtorQual(cls) {
4983
+ const sf = cls.getSourceFile();
4984
+ const base = `${moduleOf(sf)}.${namespacePrefixOf(cls)}${cls.name.text}.constructor`;
4985
+ if (!isFunctionScoped(cls)) return base;
4986
+ const impl = (cls.members ?? []).find((m) => ts.isConstructorDeclaration(m) && m.body);
4987
+ const { line, character } = sf.getLineAndCharacterOfPosition((impl ?? cls).getStart());
4988
+ return `${base}#${line + 1}:${character + 1}`;
4989
+ }
4990
+ // A class EXPRESSION is keyed by its stable tag (`<class>@…`, R944/R1016), never by its own optional name, which is
4991
+ // scoped to the class body: `return class Q {…}` twice in one module is two classes.
4992
+ function classUnitQual(cls, member) {
4993
+ const sf = cls.getSourceFile();
4994
+ const mod = moduleOf(sf);
4995
+ if (ts.isClassExpression(cls)) return `${mod}.<class>@${stableUnitTag("classexpr", cls)}.${member}`;
4996
+ const name = cls.name ? cls.name.text : "<anonymous>"; // only `export default class {}`: one per module
4997
+ const base = `${mod}.${namespacePrefixOf(cls)}${name}.${member}`;
4998
+ if (!cls.name || !isFunctionScoped(cls)) return base;
4999
+ const { line, character } = sf.getLineAndCharacterOfPosition(cls.getStart());
5000
+ return `${base}#${line + 1}:${character + 1}`;
5001
+ }
5002
+ // The `<heritage>` unit beside a class's constructor unit — one derivation for its minting and its wiring.
5003
+ function heritageQualOf(ctorQual) {
5004
+ return ctorQual && /\.constructor(#\d+:\d+)?$/.test(ctorQual)
5005
+ ? ctorQual.replace(/\.constructor(#\d+:\d+)?$/, ".<heritage>$1") : null;
5006
+ }
4893
5007
  function isFunctionScoped(node) {
4894
5008
  for (let p = node.parent; p; p = p.parent) {
4895
5009
  if (ts.isSourceFile(p) || ts.isModuleBlock(p)) return false;
@@ -5006,7 +5120,7 @@ for (const sf of sources) {
5006
5120
  for (const t of h.types) for (const idecl of localInterfaceDecls(t.expression)) climb(idecl);
5007
5121
  for (const t of h.types) for (const idecl of foreignInterfaceDecls(t.expression)) fClimb(idecl);
5008
5122
  }
5009
- const ctorQual = `${mod}.${namespacePrefixOf(node)}${node.name.text}.constructor`;
5123
+ const ctorQual = classCtorQual(node);
5010
5124
  if (!fns.has(ctorQual)) {
5011
5125
  const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
5012
5126
  fns.set(ctorQual, { local: `${node.name.text}.constructor`, direct: new Set(), fsKinds: new Set(), edges: new Set(),
@@ -5134,6 +5248,7 @@ for (const sf of sources) {
5134
5248
  // (a wholly-unresolvable target leaves only the minted unit — nothing to join a forcing site to.)
5135
5249
  }
5136
5250
  }
5251
+ if (ts.isClassExpression(node)) mintClassExprCtor(node); // ⟨R1016⟩
5137
5252
  ts.forEachChild(node, collect);
5138
5253
  })(sf);
5139
5254
  }
@@ -5265,6 +5380,93 @@ for (const { mod, name, ident } of exportAliasCandidates) {
5265
5380
  }
5266
5381
  }
5267
5382
 
5383
+ // ⟨SOUNDNESS R1016⟩ A CLASS EXPRESSION'S CONSTRUCTOR IS A UNIT, as a class declaration's always was. Without one,
5384
+ // `const L = class { constructor() { fs.writeFileSync(…) } }; new L()` and `new Holder.Inner()` over a
5385
+ // `static Inner = class {…}` had nothing to edge to: the constructing function read `['Unknown']` (`deny Fs` exit 0
5386
+ // on a write that ran — EXECUTED), and the body's `Fs` fell through `enclosing` to whatever EVALUATES the class
5387
+ // (`<module>`, `Holder.<static-init>`), which never constructs it (EXECUTED: importing the module writes nothing).
5388
+ // The unit holds what runs at construction — the explicit constructor's body and parameters, and every INSTANCE
5389
+ // field initialiser that is not itself a function value (that one runs when called, and stays where it was) —
5390
+ // so `new` resolves to it through the checker like any constructor, and an implicit constructor is found through
5391
+ // the constructed value's TYPE (`classExprCtorOfNew`). Minted only where there is something to hold.
5392
+ //
5393
+ // WHETHER THE EVALUATING UNIT KEEPS THE CHARGE is ⟨R519⟩'s question, and its answer stands: minting is an
5394
+ // ADDITION unless every construction is provably one this scan edges. A class VALUE that escapes — exported,
5395
+ // returned, passed, stored, `extends`-ed, read as a static — can be constructed where no edge reaches it (another
5396
+ // package, which cannot name an anonymous class's constructor; an `any`-typed `new`), so pass 2a′ keeps the
5397
+ // evaluator's containment edge for it. Only a `const`-bound, unexported class expression whose every reference
5398
+ // is a `new` callee (or a type position, or an `instanceof` operand) drops it — there, every run of the body is
5399
+ // a `new` the scan resolved, and the evaluator's charge was the fabrication R1016 measured.
5400
+ function mintClassExprCtor(cls) {
5401
+ const ctor = (cls.members ?? []).find((m) => ts.isConstructorDeclaration(m) && m.body);
5402
+ const fieldInit = (cls.members ?? []).some((m) => ts.isPropertyDeclaration(m) && m.initializer
5403
+ && !(ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static)
5404
+ && !ts.isArrowFunction(m.initializer) && !ts.isFunctionExpression(m.initializer));
5405
+ if (!ctor && !fieldInit) return null;
5406
+ const sf = cls.getSourceFile();
5407
+ const local = `<class>@${stableUnitTag("classexpr", cls)}.constructor`;
5408
+ const qual = `${moduleOf(sf)}.${local}`;
5409
+ if (!fns.has(qual)) {
5410
+ const at = ctor ?? cls;
5411
+ const { line, character } = sf.getLineAndCharacterOfPosition(at.getStart());
5412
+ fns.set(qual, { local, direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(), tables: new Set(),
5413
+ cmds: new Set(), paths: new Set(), blind: new Set(), incomplete: new Set(), dispatch: new Set(),
5414
+ why: new Set(), entry: false,
5415
+ loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}`,
5416
+ endLine: sf.getLineAndCharacterOfPosition(at.getEnd()).line + 1 });
5417
+ }
5418
+ if (ctor) nodeName.set(ctor, qual);
5419
+ classExprCtorQual.set(cls, qual);
5420
+ if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH mint ${qual}`);
5421
+ return qual;
5422
+ }
5423
+ // The class-expression constructor unit a `new X(…)` runs, read off the constructed value's TYPE — the route an
5424
+ // IMPLICIT constructor needs (the signature then has no declaration, or names the BASE's), and harmless beside an
5425
+ // explicit one, which the signature already names.
5426
+ function classExprCtorOfNew(node) {
5427
+ if (!classExprCtorQual.size || !ts.isNewExpression(node) || !node.expression) return null;
5428
+ let vd;
5429
+ try { vd = checker.getTypeAtLocation(node.expression)?.getSymbol?.()?.valueDeclaration; } catch { vd = undefined; }
5430
+ return vd && ts.isClassExpression(vd) ? (classExprCtorQual.get(vd) ?? null) : null;
5431
+ }
5432
+ // Does this class expression's VALUE reach anything but a `new` this scan resolves? (see `mintClassExprCtor`)
5433
+ function classExprEscapes(cls) {
5434
+ let top = cls;
5435
+ while (top.parent && (ts.isParenthesizedExpression(top.parent) || ts.isAsExpression(top.parent)
5436
+ || ts.isNonNullExpression(top.parent) || (ts.isSatisfiesExpression?.(top.parent) ?? false))) top = top.parent;
5437
+ const vd = top.parent;
5438
+ if (!vd || !ts.isVariableDeclaration(vd) || vd.initializer !== top || !ts.isIdentifier(vd.name)
5439
+ || !(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const)
5440
+ || (ts.getCombinedModifierFlags(vd) & ts.ModifierFlags.Export)) return true;
5441
+ const syms = new Set();
5442
+ const outer = checker.getSymbolAtLocation(vd.name);
5443
+ if (!outer) return true;
5444
+ syms.add(outer);
5445
+ if (cls.name) { const inner = checker.getSymbolAtLocation(cls.name); if (inner) syms.add(inner); }
5446
+ const names = new Set([vd.name.text, cls.name?.text].filter(Boolean));
5447
+ let escapes = false;
5448
+ (function visit(n) {
5449
+ if (escapes) return;
5450
+ if (ts.isIdentifier(n) && names.has(n.text) && n !== vd.name && n !== cls.name) {
5451
+ let sym; try { sym = checker.getSymbolAtLocation(n); } catch { sym = undefined; }
5452
+ if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
5453
+ const p0 = n.parent;
5454
+ if (p0 && ts.isShorthandPropertyAssignment(p0) && p0.name === n) { escapes = true; return; }
5455
+ if (sym && syms.has(sym)) {
5456
+ let cur = n;
5457
+ while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
5458
+ || ts.isAsExpression(cur.parent) || (ts.isSatisfiesExpression?.(cur.parent) ?? false))) cur = cur.parent;
5459
+ const p = cur.parent;
5460
+ const safe = !!p && ((ts.isNewExpression(p) && p.expression === cur)
5461
+ || ts.isTypeQueryNode(p)
5462
+ || (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword));
5463
+ if (!safe) { escapes = true; return; }
5464
+ }
5465
+ }
5466
+ ts.forEachChild(n, visit);
5467
+ })(vd.getSourceFile());
5468
+ return escapes;
5469
+ }
5268
5470
  // ⟨SOUNDNESS R956⟩ A class EXPRESSION's member has no unit (`localName` names class DECLARATION members only), so
5269
5471
  // its body folded into whatever evaluates the expression and no dispatch could land on it. An override needs an
5270
5472
  // addressable body: mint it the way a structural implementor's member is minted — keyed by `stableUnitTag`, with
@@ -6119,12 +6321,13 @@ function recordConformer(c, t) {
6119
6321
  }
6120
6322
  if (ts.isClassDeclaration(t)) {
6121
6323
  if (c.kind === "class" && classInSubtree(c.node, t)) return false; // nominal: the subtree already answers
6122
- // R958 — an ASSERTED downcast (`base as Sub`, the target inside the conformer's own subtree) names no new
6123
- // conformer: the receiver's class arm answers `Sub`'s subtree. Scoped to an ASSERTION on purpose. Without one,
6124
- // `f(new Base())` into `f(s: Sub)` passed the checker's own (structural) check and the value really IS a
6125
- // `Base` — refusing that would drop the body that runs. MEASURED: the guard unscoped is load-bearing (a
6126
- // structurally identical `Base` IS assignable to `Sub`), so its scope decides which of the two it decides.
6127
- if (c.kind === "class" && conversionViaAssertion && classInSubtree(t, c.node)) return false;
6324
+ // ⟨SOUNDNESS R958, the DOWNCAST half⟩ An asserted downcast (`base as Sub`) IS a new conformer when the value is a
6325
+ // `Base`, and nothing here can tell that apart from a truthful one. A refusal stood here, scoped to assertions,
6326
+ // on the claim that "the receiver's class arm answers `Sub`'s subtree" — but the value's class is `Base`, which
6327
+ // is NOT in `Sub`'s subtree, so `Base.m` was the body that ran and the one no arm named. EXECUTED: `dO(bO as
6328
+ // SubO)` wrote a file through `BaseO.m` with `dO`/`rO` ABSENT and `pure a.dO` exit 0, while `dP(bP)` — the same
6329
+ // value without the `as` — was `Fs`, exit 1. An assertion must not delete a charge the unasserted conversion
6330
+ // makes. On a TRUTHFUL downcast the registration over-charges a `Sub`-typed receiver with `Base`'s override.
6128
6331
  let added = false;
6129
6332
  for (const anc of localAncestorsAndSelf(t)) added = pushUnique(classConformers, anc, c.node) || added;
6130
6333
  if (c.kind === "lit") mintStructuralMembers(c.node, true);
@@ -6197,7 +6400,6 @@ function isConversionPosition(n) {
6197
6400
  return false;
6198
6401
  }
6199
6402
  // R958 — the value under an assertion chain: parentheses, `as`, `<T>`, `!` and `satisfies` change no runtime value.
6200
- let conversionViaAssertion = false; // set while `convert` records a source read THROUGH an assertion
6201
6403
  const peelAssertions = (e) => unwrapArgExpr(e); // R780: one wrapper set, not a third copy of it
6202
6404
  // R82 / PART 87: is the conversion's SOURCE type a fact about the VALUE, or only about a generic signature? A
6203
6405
  // call whose declared return type mentions one of its own type parameters (`wrap<T>(x: T): T`) hands back
@@ -6395,21 +6597,19 @@ for (const sf of sources) {
6395
6597
  // called as `launder(new LocalW())` ran LocalW's `m` (EXECUTED) with `deny Fs` exit 0 on the caller. The
6396
6598
  // parameter's visible arguments say what it holds — the SAME provenance index R246's accessor arm asks
6397
6599
  // (`receiverValueTypes`) — so each argument type is recorded as a conversion source. Upcasts only, as
6398
- // ever (`recordConversion` keeps a target the type is assignable to). A dependency-sourced value and a
6399
- // lying downcast are untouched: they are R958's by-design residue.
6400
- if (anyishType(st)) for (const vt of receiverValueTypes(src)) {
6600
+ // ever (`recordConversion` keeps a target the type is assignable to). ⟨R958 widened-local half⟩ and the
6601
+ // same for a source typed `object`/`{}`, and a `const` declared wider than its initializer
6602
+ // (`uninformativeType`). A dependency-sourced value is R958's disclosed residue (`invisible`).
6603
+ if (uninformativeType(st)) for (const vt of receiverValueTypes(src)) {
6401
6604
  try { recordConversion(vt, tt); } catch { /* best effort, as the main record below */ }
6402
6605
  CONFORMER_REACH?.("any-param", `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
6403
6606
  }
6404
- conversionViaAssertion = kind !== "conversion";
6405
- try {
6406
- if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
6407
- const count = () => [...interfaceImpls.values(), ...foreignInterfaceImpls.values(), ...classConformers.values(),
6408
- ...depConformers.values()].reduce((a, x) => a + x.length, 0);
6409
- const before = count();
6410
- recordConversion(st, tt);
6411
- if (count() > before) CONFORMER_REACH(kind, `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
6412
- } finally { conversionViaAssertion = false; }
6607
+ if (!CONFORMER_REACH) { recordConversion(st, tt); return; }
6608
+ const count = () => [...interfaceImpls.values(), ...foreignInterfaceImpls.values(), ...classConformers.values(),
6609
+ ...depConformers.values()].reduce((a, x) => a + x.length, 0);
6610
+ const before = count();
6611
+ recordConversion(st, tt);
6612
+ if (count() > before) CONFORMER_REACH(kind, `${path.relative(rootDir, sf.fileName)}:${at.getStart()}`);
6413
6613
  };
6414
6614
  (function walkConversions(node) {
6415
6615
  if (ts.isExpression(node) && !ts.isSpreadElement(node) && isConversionPosition(node)) {
@@ -6429,9 +6629,248 @@ for (const sf of sources) {
6429
6629
  let tt; try { tt = checker.getTypeFromTypeNode(node.type); } catch { tt = undefined; }
6430
6630
  convert(peelAssertions(node.expression), tt, node, "assertion");
6431
6631
  }
6632
+ if (ts.isObjectLiteralExpression(node)) providerSubstitution(node); // ⟨R1061⟩
6432
6633
  ts.forEachChild(node, walkConversions);
6433
6634
  })(sf);
6434
6635
  }
6636
+ // ⟨SOUNDNESS R1061⟩ A DEPENDENCY-INJECTION PROVIDER IS A CONVERSION THE CHECKER NEVER SEES. NestJS (and Angular,
6637
+ // which shares the shape) wires `{ provide: Store, useClass: FileStore }` — or `useValue: <obj>`, `useFactory: () =>
6638
+ // <obj>`, `useExisting: Other` — and the container then hands the substitute to every `store: Store` parameter. No
6639
+ // expression converts the substitute to `Store`, so a `this.store.save()` resolved to `Store.save` alone: EXECUTED on
6640
+ // NestJS 10, `FileStore.save` wrote a file while the calling method was ABSENT and `deny Fs`/`deny Fs Unknown` exited 0
6641
+ // for all three provider forms. The provider entry IS the conversion, so the substitute is registered as a conformer
6642
+ // of the token class — the union of the declared class and the visible substitute, through the R874/R954 conformer
6643
+ // arm every class dispatch already asks. Registered directly, without the checker's assignability test, because the
6644
+ // container does not apply one (a token class with a private member is not structurally assignable from its
6645
+ // substitute, and is still what gets injected). Only a token that names a LOCAL class registers; a string or symbol
6646
+ // token types its parameter by an interface or `any`, which the interface arm already answers or discloses.
6647
+ function providerSubstitution(lit) {
6648
+ const prop = (name) => lit.properties.find((p) => ts.isPropertyAssignment(p) && p.name && !ts.isComputedPropertyName(p.name)
6649
+ && (p.name.text ?? p.name.getText()) === name);
6650
+ const provide = prop("provide");
6651
+ if (!provide) return;
6652
+ const targets = providerTokenClasses(provide.initializer);
6653
+ if (targets === null) { substituteForOpenToken(lit, prop); return; }
6654
+ if (!targets.length) { // ⟨R1061⟩ a string/symbol token: joined to its `@Inject(token)` parameters after the walk
6655
+ const key = injectTokenKey(provide.initializer);
6656
+ if (key) (tokenProviders.get(key) ?? tokenProviders.set(key, []).get(key)).push({ lit, prop });
6657
+ return;
6658
+ }
6659
+ for (const target of targets) substituteFor(lit, prop, target);
6660
+ }
6661
+ // ⟨SOUNDNESS R1061⟩ A TOKEN THE CHECKER CANNOT NAME (`provide: someType` typed `Type<any>`, a parameter whose callers
6662
+ // escape the scan). The container injects the substitute wherever that token is asked for, which may be ANY class
6663
+ // token, so the substitute joins the candidates of every project class whose instance type it is ASSIGNABLE to — the
6664
+ // classes it could stand in for without breaking its callers. A union, never a narrowing: a class it cannot
6665
+ // conform to keeps its own answer. A token typed `any` (its import unresolved) needs nothing here: a parameter
6666
+ // typed by that token is `any` too, and a dispatch on it already discloses.
6667
+ function substituteForOpenToken(lit, prop) {
6668
+ const projectClassDecls = substituteForOpenToken.classes ??= (() => {
6669
+ const out = [];
6670
+ const visit = (n) => { if (ts.isClassDeclaration(n) && n.name) out.push(n); ts.forEachChild(n, visit); };
6671
+ for (const sf of sources) visit(sf);
6672
+ return out;
6673
+ })();
6674
+ if (typeof checker.isTypeAssignableTo !== "function") return;
6675
+ const subs = [];
6676
+ for (const key of ["useClass", "useExisting"]) {
6677
+ const p = prop(key);
6678
+ if (!p) continue;
6679
+ let t; try { t = checker.getTypeAtLocation(peelAssertions(p.initializer)); } catch { t = undefined; }
6680
+ for (const u of t?.isUnion?.() ? t.types : [t])
6681
+ for (const sig of u?.getConstructSignatures?.() ?? []) { try { subs.push(sig.getReturnType()); } catch { /* none */ } }
6682
+ }
6683
+ const uv = prop("useValue");
6684
+ if (uv) { try { subs.push(checker.getTypeAtLocation(peelAssertions(uv.initializer))); } catch { /* none */ } }
6685
+ const uf = prop("useFactory");
6686
+ if (uf) {
6687
+ let ft; try { ft = checker.getTypeAtLocation(uf.initializer); } catch { ft = undefined; }
6688
+ for (const sig of ft?.getCallSignatures?.() ?? []) {
6689
+ let rt; try { rt = sig.getReturnType(); rt = checker.getAwaitedType?.(rt) ?? rt; } catch { rt = undefined; }
6690
+ if (rt) subs.push(rt);
6691
+ }
6692
+ }
6693
+ for (const st of subs) for (const s of st?.isUnion?.() ? st.types : [st]) {
6694
+ if (!s || (s.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) continue;
6695
+ const c = conformerOfSourceType(s);
6696
+ if (!c || c.kind === "iface") continue;
6697
+ for (const cd of projectClassDecls) {
6698
+ if (cd === c.node) continue;
6699
+ let it; try { it = checker.getDeclaredTypeOfSymbol(checker.getSymbolAtLocation(cd.name)); } catch { it = undefined; }
6700
+ if (!it || !(it.getProperties?.().some((m) => m.flags & (ts.SymbolFlags.Method | ts.SymbolFlags.Property)))) continue;
6701
+ let ok = false; try { ok = checker.isTypeAssignableTo(s, it); } catch { ok = false; }
6702
+ if (ok && recordConformer(c, cd)) CONFORMER_REACH?.("di-open-token", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
6703
+ }
6704
+ }
6705
+ }
6706
+ // ⟨SOUNDNESS R1061, the TOKEN half⟩ WHICH CLASS A `provide:` TOKEN NAMES, asked of the CHECKER'S TYPE rather than of
6707
+ // the token's spelling. The first cut read the token's symbol and required it to BE a class declaration, so
6708
+ // `const TOK = Store6; { provide: TOK, useClass: FileStore6 }` named a variable and registered nothing: EXECUTED on
6709
+ // NestJS 10, `FileStore6.save` wrote its file, `runStore6` was ABSENT and `deny Fs`/`deny Fs Unknown` exited 0. The
6710
+ // value of the token at runtime is a class CONSTRUCTOR, and its type says which: `typeof Store6` through a const, an
6711
+ // import, an object member, a parameter fed at visible sites, or a `.map` over `[Store6]` alike. Returns the project
6712
+ // classes the token may be (a union token yields each), [] when the token is provably not a class constructor (a
6713
+ // string, a symbol, an `InjectionToken` instance — the interface arm's question), or null when the checker cannot say.
6714
+ function providerTokenClasses(expr, depth = 0, deps = false) {
6715
+ const e = peelAssertions(expr);
6716
+ if (!e || depth > 6) return null;
6717
+ let tsym; try { tsym = checker.getSymbolAtLocation(e); } catch { tsym = undefined; }
6718
+ if (tsym && (tsym.flags & ts.SymbolFlags.Alias)) { try { tsym = checker.getAliasedSymbol(tsym); } catch { tsym = undefined; } }
6719
+ const direct = (tsym?.declarations ?? []).find((d) => ts.isClassDeclaration(d) && (isProjectNode(d) || (deps && d.getSourceFile().isDeclarationFile)));
6720
+ if (direct) return [direct];
6721
+ // ⟨R1061⟩ THE VALUE BEFORE THE ANNOTATION. `const T: typeof Store = FileStore` holds FileStore; its type says Store.
6722
+ // A `const` binding answers with its initializer (and a const object literal's member with that member's value);
6723
+ // a reassignable binding's value is not its declaration's, so the type cannot be trusted either — open.
6724
+ const vd = tsym?.valueDeclaration;
6725
+ if (vd && ts.isVariableDeclaration(vd) && isProjectNode(vd)) {
6726
+ if (!(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) || !vd.initializer) return null;
6727
+ if (ts.isIdentifier(vd.name)) return providerTokenClasses(vd.initializer, depth + 1, deps);
6728
+ }
6729
+ if (vd && ts.isPropertyAssignment(vd) && isProjectNode(vd) && ts.isPropertyAccessExpression(e)) {
6730
+ const recv = unwrapArgExpr(e.expression);
6731
+ let rs; try { rs = ts.isIdentifier(recv) ? checker.getSymbolAtLocation(recv) : undefined; } catch { rs = undefined; }
6732
+ const rd = rs?.valueDeclaration;
6733
+ if (rd && ts.isVariableDeclaration(rd) && (ts.getCombinedNodeFlags(rd) & ts.NodeFlags.Const) && rd.initializer
6734
+ && unwrapArgExpr(rd.initializer) === vd.parent) return providerTokenClasses(vd.initializer, depth + 1, deps);
6735
+ }
6736
+ let t; try { t = checker.getTypeAtLocation(e); } catch { return null; }
6737
+ if (!t) return null;
6738
+ const out = [];
6739
+ let open = false;
6740
+ for (const u of t.isUnion?.() ? t.types : [t]) {
6741
+ if (u.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.BooleanLike
6742
+ | ts.TypeFlags.Undefined | ts.TypeFlags.Null)) continue;
6743
+ if (!u.getConstructSignatures?.().length) {
6744
+ // A type that can HOLD a class without saying which — `Function`, `Object`, `{}`, `object`, `any`, `unknown`.
6745
+ const nm = u.getSymbol?.()?.getName?.();
6746
+ const libRoot = (nm === "Function" || nm === "Object" || nm === "CallableFunction" || nm === "NewableFunction")
6747
+ && (u.getSymbol().declarations ?? []).every((d) => d.getSourceFile().isDeclarationFile && !isProjectNode(d));
6748
+ const emptyObj = (u.flags & ts.TypeFlags.Object) && !u.getProperties?.().length && !u.getCallSignatures?.().length;
6749
+ if (libRoot || emptyObj || (u.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive | ts.TypeFlags.TypeParameter))) {
6750
+ open = true; continue;
6751
+ }
6752
+ // an object that is not a constructor (an `InjectionToken`, a plain token object) cannot be a class token
6753
+ if ((u.flags & ts.TypeFlags.Object) && !(u.getCallSignatures?.().length)) continue;
6754
+ open = true; continue;
6755
+ }
6756
+ const cd = (u.getSymbol?.()?.declarations ?? []).find((d) => ts.isClassDeclaration(d));
6757
+ if (cd && (isProjectNode(cd) || (deps && cd.getSourceFile().isDeclarationFile))) { if (!out.includes(cd)) out.push(cd); continue; }
6758
+ if (cd) continue; // a dependency's class as a TOKEN: its own dispatch is the dependency's ledger
6759
+ open = true; // a constructor TYPE with no class behind it (`Type<any>`, `new () => T`, `Function`)
6760
+ }
6761
+ if (open && ts.isIdentifier(e)) {
6762
+ // a PARAMETER whose type does not say (`token: Type<any>`): the classes its visible call sites pass, when the
6763
+ // function does not escape — a helper `provider(Store, FileStore)` is the shape.
6764
+ let ps = checker.getSymbolAtLocation(e);
6765
+ if (ps && (ps.flags & ts.SymbolFlags.Alias)) { try { ps = checker.getAliasedSymbol(ps); } catch { /* keep */ } }
6766
+ const args = ps && paramArgs.get(ps);
6767
+ if (args?.length && !paramFnEscaped(paramOwnerOf.get(ps))) {
6768
+ const viaArgs = [];
6769
+ for (const a of args) {
6770
+ const r = a ? providerTokenClasses(a, depth + 1, deps) : null;
6771
+ if (r === null) return out.length ? out : null;
6772
+ for (const c of r) if (!viaArgs.includes(c)) viaArgs.push(c);
6773
+ }
6774
+ for (const c of viaArgs) if (!out.includes(c)) out.push(c);
6775
+ return out;
6776
+ }
6777
+ }
6778
+ return open ? (out.length ? out : null) : out;
6779
+ }
6780
+ function substituteFor(lit, prop, target) {
6781
+ const sources = [];
6782
+ let open = false; // a substitute this scan cannot name stands in for `target`
6783
+ // ⟨R1061⟩ The INSTANCE types a `useClass`/`useExisting` value constructs, asked as the token is: the class symbol,
6784
+ // else the class(es) the expression's TYPE or its visible call sites name — `const IMPL = FileStore` and a helper
6785
+ // `prov(Store, FileStore)` alike. A constructor value the checker cannot name (`Type<Store>`, `any`) is OPEN.
6786
+ const fromCtorValue = (e) => {
6787
+ const cls = providerTokenClasses(e, 0, true);
6788
+ if (cls === null) { open = true; return; }
6789
+ for (const cd of cls) {
6790
+ let it; try { it = checker.getDeclaredTypeOfSymbol(cd.name ? checker.getSymbolAtLocation(cd.name) : cd.symbol); } catch { it = undefined; }
6791
+ if (it) sources.push(it); else open = true;
6792
+ }
6793
+ };
6794
+ for (const key of ["useClass", "useExisting"]) { const p = prop(key); if (p) fromCtorValue(p.initializer); }
6795
+ const anyish = (t) => !t || !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown));
6796
+ const uv = prop("useValue");
6797
+ if (uv) { let t; try { t = checker.getTypeAtLocation(peelAssertions(uv.initializer)); } catch { t = undefined; } if (anyish(t)) open = true; else sources.push(t); }
6798
+ const uf = prop("useFactory");
6799
+ if (uf) {
6800
+ let ft; try { ft = checker.getTypeAtLocation(uf.initializer); } catch { ft = undefined; }
6801
+ const sigs = ft?.getCallSignatures?.() ?? [];
6802
+ if (!sigs.length) open = true;
6803
+ for (const sig of sigs) {
6804
+ let rt; try { rt = sig.getReturnType(); rt = checker.getAwaitedType?.(rt) ?? rt; } catch { rt = undefined; }
6805
+ if (anyish(rt)) open = true; else sources.push(rt);
6806
+ }
6807
+ }
6808
+ for (const st of sources) for (const s of st?.isUnion?.() ? st.types : [st]) {
6809
+ const c = conformerOfSourceType(s);
6810
+ if (c && recordConformer(c, target)) CONFORMER_REACH?.("di-provider", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
6811
+ }
6812
+ if (open && ts.isClassDeclaration(target)) {
6813
+ for (const a of localAncestorsAndSelf(target)) openSubstitutedClasses.add(a);
6814
+ CONFORMER_REACH?.("di-open-substitute", `${path.relative(rootDir, lit.getSourceFile().fileName)}:${lit.getStart()}`);
6815
+ }
6816
+ }
6817
+ // ⟨SOUNDNESS R1061, the STRING-TOKEN half⟩ `{ provide: "STORE", useClass: FileStore }` injected as
6818
+ // `@Inject("STORE") store: Store` — a parameter typed by a project CLASS under a non-class token. Nothing converts
6819
+ // FileStore to Store, so `this.store.save()` resolved to the pure `Store.save`: EXECUTED on NestJS 10, the substitute
6820
+ // wrote its file with the caller ABSENT, `deny Fs`/`deny Fs Unknown` exit 0. The token is the join key — its string
6821
+ // literal value (so `"STORE"` and `const STORE_TOKEN = "STORE"` match), else the declaration of the unique symbol or
6822
+ // const it names. Every provider under the key substitutes the parameter's declared type; a class-typed parameter
6823
+ // whose key NO visible provider registers (the list is built where this scan cannot see — a dependency's
6824
+ // `forRoot`) is an OPEN substitution and its dispatch discloses.
6825
+ function injectTokenKey(expr) {
6826
+ const e = peelAssertions(expr);
6827
+ if (!e) return null;
6828
+ let t; try { t = checker.getTypeAtLocation(e); } catch { t = undefined; }
6829
+ if (t && (t.flags & ts.TypeFlags.StringLiteral)) return `s:${t.value}`;
6830
+ if (t && (t.flags & ts.TypeFlags.NumberLiteral)) return `n:${t.value}`;
6831
+ let sym; try { sym = ts.isPropertyAccessExpression(e) ? checker.getSymbolAtLocation(e.name) : checker.getSymbolAtLocation(e); } catch { sym = undefined; }
6832
+ if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* keep */ } }
6833
+ const d = sym?.valueDeclaration;
6834
+ if (!d || !(ts.isVariableDeclaration(d) || ts.isPropertyAssignment(d) || ts.isPropertyDeclaration(d))) return null;
6835
+ if (!tokenDeclIds.has(d)) tokenDeclIds.set(d, tokenDeclIds.size + 1);
6836
+ return `d:${tokenDeclIds.get(d)}`;
6837
+ }
6838
+ {
6839
+ const injectParams = [];
6840
+ const visitInject = (n) => {
6841
+ if (ts.isParameter(n) && n.type) {
6842
+ for (const dec of ts.getDecorators?.(n) ?? []) {
6843
+ const ce = dec.expression;
6844
+ if (ts.isCallExpression(ce) && ts.isIdentifier(ce.expression) && ce.expression.text === "Inject" && ce.arguments.length === 1) {
6845
+ const key = injectTokenKey(ce.arguments[0]);
6846
+ if (key) injectParams.push({ param: n, key });
6847
+ }
6848
+ }
6849
+ }
6850
+ ts.forEachChild(n, visitInject);
6851
+ };
6852
+ for (const sf of sources) visitInject(sf);
6853
+ for (const { param, key } of injectParams) {
6854
+ let tt; try { tt = checker.getTypeFromTypeNode(param.type); } catch { tt = undefined; }
6855
+ const targets = [];
6856
+ for (const u of tt?.isUnion?.() ? tt.types : [tt]) {
6857
+ const d = (u?.getSymbol?.()?.declarations ?? []).find((x) => (ts.isClassDeclaration(x) || ts.isInterfaceDeclaration(x)) && isProjectNode(x));
6858
+ if (d && !targets.includes(d)) targets.push(d);
6859
+ }
6860
+ if (!targets.length) continue;
6861
+ const provs = tokenProviders.get(key) ?? [];
6862
+ for (const target of targets) {
6863
+ if (!provs.length) {
6864
+ if (ts.isClassDeclaration(target)) {
6865
+ for (const a of localAncestorsAndSelf(target)) openSubstitutedClasses.add(a);
6866
+ CONFORMER_REACH?.("di-inject-unprovided", `${path.relative(rootDir, param.getSourceFile().fileName)}:${param.getStart()}`);
6867
+ }
6868
+ continue;
6869
+ }
6870
+ for (const { lit, prop } of provs) substituteFor(lit, prop, target);
6871
+ }
6872
+ }
6873
+ }
6435
6874
  // The interface-flow fixpoint: a `J`-typed value converted to `X` carries every conformer of `J` with it.
6436
6875
  for (let changed = true, g = 0; changed && g++ < 32; ) {
6437
6876
  changed = false;
@@ -6590,6 +7029,19 @@ function keyLiteralNames(t, depth = 0, seen = new Set()) {
6590
7029
  // the accessor edges the value's type declares — and never hedges; a receiver neither rule answers is
6591
7030
  // exactly as it was (the residual is stated in SOUNDNESS R246, not hidden here).
6592
7031
  function anyishType(t) { return !!t && !!(t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)); } // hoisted: the conversion walk asks it first
7032
+ // ⟨SOUNDNESS R958, the WIDENED-LOCAL half⟩ A type that names no member is as silent about the value as `any`: a local
7033
+ // declared `object` or `{}` holding `new LocalW()` and asserted into an interface (`qDisp(o as Sink)`) ran
7034
+ // `LocalW.m` (EXECUTED) with every unit `[]` and no disclosure — `anyishType` alone let it through. Asked by the
7035
+ // conversion walk and the R246 receiver question alike; it only ever lets MORE value types be recorded.
7036
+ function uninformativeType(t) {
7037
+ if (!t) return false;
7038
+ if (anyishType(t) || (t.flags & ts.TypeFlags.NonPrimitive)) return true;
7039
+ if (!(t.flags & ts.TypeFlags.Object) || t.isUnion?.() || t.isIntersection?.()) return false;
7040
+ try {
7041
+ return checker.getPropertiesOfType(t).length === 0 && t.getCallSignatures().length === 0
7042
+ && t.getConstructSignatures().length === 0 && !checker.getIndexInfosOfType?.(t)?.length;
7043
+ } catch { return false; }
7044
+ }
6593
7045
  function receiverValueTypes(expr, depth = 0, seen = new Set()) {
6594
7046
  if (!expr || depth > 6) return [];
6595
7047
  let t; try { t = checker.getTypeAtLocation(expr); } catch { t = null; }
@@ -6599,10 +7051,18 @@ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
6599
7051
  let ti; try { ti = checker.getTypeAtLocation(inner); } catch { ti = null; }
6600
7052
  return [...(ti && !anyishType(ti) ? [ti] : []), ...below];
6601
7053
  }
6602
- if (!anyishType(t) || !ts.isIdentifier(expr)) return [];
7054
+ if (!uninformativeType(t) || !ts.isIdentifier(expr)) return [];
6603
7055
  const sym = checker.getSymbolAtLocation(expr);
6604
7056
  const d = sym?.valueDeclaration;
6605
- if (!sym || !d || !ts.isParameter(d) || seen.has(sym)) return [];
7057
+ if (!sym || !d || seen.has(sym)) return [];
7058
+ // ⟨R958, widened-local half⟩ a `const` declared wider than its initializer holds the initializer's value.
7059
+ if (ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.initializer
7060
+ && (ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)) {
7061
+ seen.add(sym);
7062
+ let ti; try { ti = checker.getTypeAtLocation(d.initializer); } catch { ti = null; }
7063
+ return [...(ti && !anyishType(ti) ? [ti] : []), ...receiverValueTypes(d.initializer, depth + 1, seen)];
7064
+ }
7065
+ if (!ts.isParameter(d)) return [];
6606
7066
  seen.add(sym);
6607
7067
  const args = paramArgs.get(sym);
6608
7068
  if (!args || paramFnEscaped(paramOwnerOf.get(sym))) return [];
@@ -6610,7 +7070,7 @@ function receiverValueTypes(expr, depth = 0, seen = new Set()) {
6610
7070
  for (const a of args) {
6611
7071
  if (!a) continue;
6612
7072
  let ta; try { ta = checker.getTypeAtLocation(a); } catch { ta = null; }
6613
- if (ta && !anyishType(ta)) out.push(ta);
7073
+ if (ta && !anyishType(ta)) out.push(ta); // a VALUE's own type, even `{}`, is a fact about it
6614
7074
  out.push(...receiverValueTypes(a, depth + 1, seen));
6615
7075
  }
6616
7076
  return out;
@@ -7197,13 +7657,11 @@ function moduleUnit(sf) {
7197
7657
  // body's effects otherwise walked up in `enclosing` to the ClassDeclaration, which maps to the
7198
7658
  // `C.constructor` unit, so a static-init effect was MISLABELED as the instance ctor (and carried no
7199
7659
  // unitKind). Mint it as its own unit, lazily, mirroring `moduleUnit`. (An anonymous class expression's
7200
- // static block keys under `<anonymous>`; there is at most one static-init unit per class name.)
7660
+ // static block keyed under `<anonymous>` — one unit for every such class in the module, R1067; see `classUnitQual`.)
7201
7661
  // The qual alone, WITHOUT minting — R782/R785's wiring pass asks "does this block's unit exist?" and must
7202
7662
  // not create an empty one to find out. One derivation for both, so the two cannot spell it differently.
7203
7663
  function staticBlockQual(node) {
7204
- const cls = node.parent;
7205
- const cname = (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) && cls.name ? cls.name.text : "<anonymous>";
7206
- return `${moduleOf(node.getSourceFile())}.${cname}.<static-init>`;
7664
+ return classUnitQual(node.parent, "<static-init>"); // ⟨R1067⟩ one unit per CLASS, not per class name
7207
7665
  }
7208
7666
  function staticBlockUnit(node) {
7209
7667
  const sf = node.getSourceFile();
@@ -7256,7 +7714,8 @@ function decoratorArgUnit(callNode) {
7256
7714
  // that writes in its generated constructor (EXECUTED) went from `invisible:['mixdep']` to ABSENT, and nest's six
7257
7715
  // `extends PartialType(…)` DTO constructors lost their `@nestjs/mapped-types` disclosure the same way.
7258
7716
  function heritageUnit(cls, ctorQual) {
7259
- const qual = ctorQual.replace(/\.constructor$/, ".<heritage>");
7717
+ const qual = heritageQualOf(ctorQual);
7718
+ if (!qual) return ctorQual; // not a constructor unit's key: attribute where the climb already was
7260
7719
  if (!fns.has(qual)) {
7261
7720
  const sf = cls.getSourceFile();
7262
7721
  const h = (cls.heritageClauses ?? []).find((c) => c.token === ts.SyntaxKind.ExtendsKeyword) ?? cls;
@@ -7327,6 +7786,12 @@ function enclosing(node) {
7327
7786
  && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
7328
7787
  && p.parent && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent)))
7329
7788
  { R815_REACH?.("static"); return staticBlockUnit(p); }
7789
+ // ⟨R1016⟩ an INSTANCE field initialiser of a class expression runs at construction — its constructor unit.
7790
+ if (prev && ts.isPropertyDeclaration(p) && p.initializer === prev
7791
+ && !(ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static)
7792
+ && !ts.isArrowFunction(prev) && !ts.isFunctionExpression(prev)
7793
+ && p.parent && ts.isClassExpression(p.parent) && classExprCtorQual.has(p.parent))
7794
+ return classExprCtorQual.get(p.parent);
7330
7795
  if (ts.isHeritageClause(p) && p.token === ts.SyntaxKind.ExtendsKeyword && p.parent
7331
7796
  && (ts.isClassDeclaration(p.parent) || ts.isClassExpression(p.parent))) {
7332
7797
  const ctor = nodeName.get(p.parent);
@@ -8342,8 +8807,8 @@ function isStringifyingSink(call) {
8342
8807
  const member = ex.name?.getText?.();
8343
8808
  // (a) `console.*` — every console method runs its arguments through util.format/inspect.
8344
8809
  if (ts.isIdentifier(ex.expression) && ex.expression.getText() === "console") {
8345
- const d = declOfSym(checker.getSymbolAtLocation(ex.expression));
8346
- return !d || !declIsLocal(d); // the GLOBAL console; a local `const console = …` shadow is not a sink
8810
+ // the GLOBAL console; a local `const console = …` shadow is not a sink (⟨R1074⟩ an expando write is not one)
8811
+ return !projectShadows(checker.getSymbolAtLocation(ex.expression)?.declarations);
8347
8812
  }
8348
8813
  // (b) a logging LEVEL-method on an EXTERNAL receiver — pino/winston/bunyan/log4js/debug, the direct
8349
8814
  // analogue of the SLF4J parameterized call this vein was found on. A LOCAL logger is excluded: its
@@ -8841,6 +9306,30 @@ const declImportsNodeProcess = (decl) => {
8841
9306
  // downstream consult — the fix in one place, per brief §F1 item 3 ("make the two paths share one
8842
9307
  // authority", not patch the losing copy). Populated by the pre-pass immediately below.
8843
9308
  const processAliasSymbols = new Set();
9309
+ // ⟨SOUNDNESS R1075⟩ AN IDENTIFIER IMPORTED FROM NODE'S `process` MODULE. `import { env } from "node:process"` binds the
9310
+ // environment object itself, `import { argv }` the argument vector, and `import p from` / `import * as p from` the
9311
+ // process object — under ANY local name. The identity tests below asked for the spelling `process` (and the default
9312
+ // import only when its local name was `process`), so every one of these was ABSENT: EXECUTED (`fx75`), `env.SECRET`,
9313
+ // `envAlias.SECRET`, `nsp.env.SECRET` and `procDefault.env.SECRET` each printed the planted value, each function was
9314
+ // missing from the report and `deny Env` exited 0; rollup's `src/utils/colors.ts` reads `env.FORCE_COLOR` this way.
9315
+ // Asked of the import's TARGET, not its spelling: the alias resolves to node's own `process` variable or to a member
9316
+ // of its `Process` interface, which also covers a project re-export of either. Returns "process", the member name,
9317
+ // or null. A declaration position (the import specifier itself, an export specifier) is not a reference.
9318
+ const processModuleBinding = (id) => {
9319
+ if (!id || !ts.isIdentifier(id)) return null;
9320
+ const p = id.parent;
9321
+ if (p && (ts.isImportSpecifier(p) || ts.isExportSpecifier(p) || ts.isImportClause(p) || ts.isNamespaceImport(p)
9322
+ || ts.isImportEqualsDeclaration(p) || ts.isNamespaceExport?.(p))) return null;
9323
+ let sym = checker.getSymbolAtLocation(id);
9324
+ if (!sym || !(sym.flags & ts.SymbolFlags.Alias)) return null;
9325
+ try { sym = checker.getAliasedSymbol(sym); } catch { return null; }
9326
+ const decls = sym?.declarations ?? [];
9327
+ const external = (d) => !projectFiles.has(path.resolve(d.getSourceFile().fileName));
9328
+ if (decls.some((d) => external(d) && ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.name.text === "process")) return "process";
9329
+ const m = decls.find((d) => external(d) && (ts.isPropertySignature(d) || ts.isMethodSignature(d) || ts.isPropertyDeclaration(d))
9330
+ && d.parent && ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Process");
9331
+ return m && m.name && ts.isIdentifier(m.name) ? m.name.text : null;
9332
+ };
8844
9333
  const identIsGlobalProcess = (id0) => {
8845
9334
  // ⟨SOUNDNESS R936⟩ through the ONE transparent-wrapper set (R780): `(process as any).argv`, `(<any>process).argv`
8846
9335
  // and even `(process).argv` were ABSENT because the argv arm handed this the wrapped node and this matched an
@@ -8862,14 +9351,15 @@ const identIsGlobalProcess = (id0) => {
8862
9351
  const root = unwrapArgExpr(id.expression); // R780: `(<any>globalThis).process` is the same object
8863
9352
  if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
8864
9353
  const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
8865
- return !gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
9354
+ return !projectShadows(gd); // ⟨R1074⟩
8866
9355
  }
8867
9356
  }
8868
9357
  if (!ts.isIdentifier(id)) return false;
9358
+ if (processModuleBinding(id) === "process") return true; // ⟨R1075⟩ `import p from "node:process"`, any name
8869
9359
  if (id.text === "process") {
8870
9360
  const decls = checker.getSymbolAtLocation(id)?.declarations ?? [];
8871
9361
  if (decls.some(declImportsNodeProcess)) return true; // `import process from 'node:process'`
8872
- if (!decls.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) return true; // ambient global
9362
+ if (!projectShadows(decls)) return true; // ambient global ⟨R1074⟩: an expando write is not a shadow
8873
9363
  }
8874
9364
  // A local binding under ANY name (`p`, `process`, whatever) whose sole initializer is confirmed —
8875
9365
  // by the pre-pass below — to be the global process object itself.
@@ -8900,7 +9390,7 @@ const identIsGlobalProcess = (id0) => {
8900
9390
  const root = unwrapArgExpr(node.initializer); // R780
8901
9391
  if (ts.isIdentifier(root) && (root.text === "globalThis" || root.text === "global")) {
8902
9392
  const gd = checker.getSymbolAtLocation(root)?.declarations ?? [];
8903
- if (!gd.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
9393
+ if (!projectShadows(gd)) { // ⟨R1074⟩
8904
9394
  for (const el of node.name.elements) {
8905
9395
  const propName = el.propertyName ? (ts.isIdentifier(el.propertyName) ? el.propertyName.text : null)
8906
9396
  : (ts.isIdentifier(el.name) ? el.name.text : null);
@@ -8944,6 +9434,7 @@ const unwrapEnvTransparent = (e) => unwrapArgExpr(e); // R780: one wrapper set
8944
9434
  // project-local `process` (a parameter, a module `const`) matches nothing.
8945
9435
  const isProcessEnvExpr = (expr) => {
8946
9436
  if (!expr) return false;
9437
+ if (ts.isIdentifier(expr) && processModuleBinding(expr) === "env") return true; // ⟨R1075⟩ `import { env } from "node:process"`
8947
9438
  let obj = null;
8948
9439
  if (ts.isPropertyAccessExpression(expr) && expr.name.text === "env") obj = expr.expression;
8949
9440
  else if (ts.isElementAccessExpression(expr) && expr.argumentExpression
@@ -8961,7 +9452,8 @@ const isProcessEnvExpr = (expr) => {
8961
9452
  // it "flooded a scala-library scan with a spurious 14k Env" — JVM `-D` config read pervasively at
8962
9453
  // class-init. `argv` is read once in a main.)
8963
9454
  const isProcessArgvExpr = (expr) =>
8964
- expr && ts.isPropertyAccessExpression(expr) && expr.name.text === "argv" && identIsGlobalProcess(expr.expression);
9455
+ expr && ((ts.isPropertyAccessExpression(expr) && expr.name.text === "argv" && identIsGlobalProcess(expr.expression))
9456
+ || (ts.isIdentifier(expr) && processModuleBinding(expr) === "argv")); // ⟨R1075⟩ `import { argv } from "node:process"`
8965
9457
 
8966
9458
  // The LEAVES a value can come from: through the transparent wrappers, both arms of `?:`, either side of
8967
9459
  // `??`/`||`, the right of `&&`/`,`, and the right of an assignment expression. A merge is a MAY-flow, and
@@ -9034,12 +9526,90 @@ const envValueLeaves = (expr, out = []) => {
9034
9526
  // this adds the READERS and removes nothing.
9035
9527
  const envAliasSymbols = new Set();
9036
9528
  const envMayAliasSymbols = new Set();
9529
+ // ⟨SOUNDNESS R1075⟩ the symbol a REFERENCE names, through an import: `import { myEnv } from "./cfg"` over
9530
+ // `export const myEnv = process.env` is the exported const, whose symbol is in the alias sets — the import's own
9531
+ // alias symbol never is. EXECUTED (`fx75c`): `viaExportedAlias() { return myEnv.SECRET }` printed the planted value
9532
+ // and was ABSENT, `deny Env` exit 0, while the same read in the declaring module is `['Env']`.
9533
+ const envValueSymAt = (id) => {
9534
+ let sym = checker.getSymbolAtLocation(id);
9535
+ if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* unresolved */ } }
9536
+ return sym;
9537
+ };
9037
9538
  // ⟨R935⟩ property locations, kept apart from the local sets so a property name never reaches the
9038
9539
  // identifier-keyed consumers (2c's `envSourceKind`, the alias-binding inert rule) by accident.
9039
9540
  const envPropMustSymbols = new Set();
9040
9541
  const envPropMaySymbols = new Set();
9041
9542
  // ⟨R935⟩ a property location's identity: its first declaration node (see `memberSymOf` for why not the symbol).
9042
9543
  const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
9544
+ // ⟨SOUNDNESS R935, the RECEIVER half⟩ THE LOCATION A MEMBER READ NAMES IS THE OBJECT THE RECEIVER HOLDS, not the
9545
+ // member its DECLARED TYPE declares. The first cut keyed a read on the checker's property symbol, which is the
9546
+ // literal's own property only when the binding's type was INFERRED from the literal. Give the binding a type —
9547
+ // `const icfg: Cfg = { env: process.env }` — and `icfg.env` resolves to `Cfg`'s `PropertySignature`, an open set
9548
+ // that is no location at all, so `viaIface() { return icfg.env.SECRET }` was ABSENT with `deny Env viaIface` exit
9549
+ // 0 over a read that printed the planted secret (EXECUTED). An ARRAY literal had no location to key at all:
9550
+ // `const arr = [process.env]; arr[0].SECRET`, same measurement. Where the receiver is an identifier bound by a
9551
+ // `const` to an object or array literal, the object IS that literal, so the read is keyed on the literal's own
9552
+ // member — the last property of that name (a later spread makes it unresolvable), or the element at a literal
9553
+ // index with no spread or hole before it. Everything that can REBIND such a member off the books is a binding
9554
+ // the fixpoint must see, or the location is charged over a replaced value:
9555
+ // · an OBJECT literal's member is written through its declared member too (`icfg.env = {}` names `Cfg.env`,
9556
+ // as does any other `Cfg`-typed object), so a read resolved this way is MAY wherever that declared member
9557
+ // is ever written or reflectively written (`envDeclWritten`, filled by the binding walk below);
9558
+ // · an ARRAY's elements move under any method call or escape, so an element is MUST only while every
9559
+ // reference to the array is a literal-index READ; any other reference makes every element MAY.
9560
+ // MAY discloses `Unknown[env-maybe-read]`, as every other location does. A `let` binding, a receiver that is a
9561
+ // parameter or a property, and a read through iteration (`for…of`, `.map`) are not resolved here.
9562
+ const envDeclWritten = new Set();
9563
+ let envBindingTable = null; // ⟨R935⟩ the binding table, published for the typed floor (`envFloorOpaque`)
9564
+ const envLitElems = new Map(); // array-literal element node -> true while every reference is an index read
9565
+ const envConstLiteral = (id) => {
9566
+ if (!id || !ts.isIdentifier(id)) return null;
9567
+ let sym = checker.getSymbolAtLocation(id);
9568
+ if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { return null; } }
9569
+ const d = sym?.valueDeclaration;
9570
+ if (!d || !ts.isVariableDeclaration(d) || !ts.isIdentifier(d.name) || !d.initializer
9571
+ || !(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)
9572
+ || !projectFiles.has(path.resolve(d.getSourceFile().fileName))) return null;
9573
+ const lit = unwrapArgExpr(d.initializer);
9574
+ return lit && (ts.isObjectLiteralExpression(lit) || ts.isArrayLiteralExpression(lit)) ? lit : null;
9575
+ };
9576
+ const envMemberName = (p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteralLike(p.name)
9577
+ || ts.isNumericLiteral(p.name) || ts.isPrivateIdentifier(p.name))) ? p.name.text : null;
9578
+ // -> { key, declKey } for a member read whose receiver is a const literal, else null.
9579
+ const envLiteralMember = (n) => {
9580
+ let recv, name = null, idx = null;
9581
+ if (ts.isPropertyAccessExpression(n)) { recv = n.expression; name = n.name.text; }
9582
+ else if (ts.isElementAccessExpression(n) && n.argumentExpression) {
9583
+ recv = n.expression;
9584
+ if (ts.isStringLiteralLike(n.argumentExpression)) name = n.argumentExpression.text;
9585
+ else if (ts.isNumericLiteral(n.argumentExpression)) idx = Number(n.argumentExpression.text);
9586
+ else return null;
9587
+ } else return null;
9588
+ const lit = envConstLiteral(unwrapArgExpr(recv));
9589
+ if (!lit) return null;
9590
+ if (ts.isArrayLiteralExpression(lit)) {
9591
+ if (idx === null || !Number.isInteger(idx) || idx >= lit.elements.length) return null;
9592
+ for (let i = 0; i <= idx; i++) {
9593
+ const el = lit.elements[i];
9594
+ if (ts.isSpreadElement(el) || ts.isOmittedExpression(el)) return null;
9595
+ }
9596
+ return { key: lit.elements[idx], declKey: null };
9597
+ }
9598
+ const want = name ?? (idx !== null ? String(idx) : null);
9599
+ if (want === null) return null;
9600
+ let hit = null;
9601
+ for (const p of lit.properties) {
9602
+ if (ts.isSpreadAssignment(p)) hit = p;
9603
+ else if (envMemberName(p) === want) hit = p;
9604
+ }
9605
+ if (!hit || !(ts.isPropertyAssignment(hit) || ts.isShorthandPropertyAssignment(hit))) return null;
9606
+ const key = envLocKey(checker.getSymbolAtLocation(hit.name));
9607
+ if (!key) return null;
9608
+ let raw = null;
9609
+ try { raw = ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
9610
+ : (n.argumentExpression ? checker.getSymbolAtLocation(n.argumentExpression) : null); } catch { raw = null; }
9611
+ return { key, declKey: envLocKey(raw) };
9612
+ };
9043
9613
  {
9044
9614
  const ENV_DESTRUCTURED = Symbol("env-destructured");
9045
9615
  const bindings = new Map(); // symbol -> [value expr | null | ENV_DESTRUCTURED | { same: symbol }]
@@ -9056,7 +9626,7 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
9056
9626
  const memberRawSym = (n) => (ts.isPropertyAccessExpression(n) ? checker.getSymbolAtLocation(n.name)
9057
9627
  : (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
9058
9628
  ? checker.getSymbolAtLocation(n.argumentExpression) : null) ?? null;
9059
- const memberSymOf = (n) => envLocKey(memberRawSym(n));
9629
+ const memberSymOf = (n) => envLiteralMember(n)?.key ?? envLocKey(memberRawSym(n));
9060
9630
  // Is this property symbol a location whose bindings we can enumerate? Every declaration must be
9061
9631
  // value-bearing, in a project file, and not a member of an extended class.
9062
9632
  const propEligible = (sym) => {
@@ -9073,7 +9643,12 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
9073
9643
  if (!ok) propIneligible.add(sym);
9074
9644
  return ok;
9075
9645
  };
9076
- const noteProp = (sym, v) => { if (propEligible(sym)) { const k = envLocKey(sym); propSyms.add(k); note(k, v); } };
9646
+ const noteProp = (sym, v) => {
9647
+ if (propEligible(sym)) { const k = envLocKey(sym); propSyms.add(k); note(k, v); }
9648
+ else if (sym && v !== undefined) envDeclWritten.add(envLocKey(sym)); // ⟨R935⟩ see `envLiteralMember`
9649
+ };
9650
+ // ⟨R935⟩ a WRITE through a member that is not itself a location still rebinds whatever object it lands on.
9651
+ const noteWrite = (sym, v) => { if (sym) { envDeclWritten.add(envLocKey(sym)); noteProp(sym, v); } };
9077
9652
  const collect = (node) => {
9078
9653
  if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
9079
9654
  note(checker.getSymbolAtLocation(node.name), node.initializer ?? null);
@@ -9091,7 +9666,10 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
9091
9666
  && (ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left))) {
9092
9667
  // ⟨R935⟩ an assignment to a property location; a compound operator (`+=`) binds a non-environment.
9093
9668
  const s = memberRawSym(node.left);
9094
- if (s) noteProp(s, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null);
9669
+ if (s) noteWrite(s, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null);
9670
+ // ⟨R935⟩ …and the const literal's own member, when the receiver resolves to one.
9671
+ const lm = envLiteralMember(node.left);
9672
+ if (lm && !ts.isArrayLiteralExpression(lm.key.parent)) { propSyms.add(lm.key); note(lm.key, LOGICAL_ASSIGN.has(node.operatorToken.kind) ? node.right : null); }
9095
9673
  } else if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {
9096
9674
  const s = checker.getSymbolAtLocation(node.name);
9097
9675
  if (s && (s.flags & ts.SymbolFlags.Property)) noteProp(s, node.initializer);
@@ -9126,27 +9704,80 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
9126
9704
  if (reflectTarget) {
9127
9705
  let props = [];
9128
9706
  try { props = checker.getTypeAtLocation(reflectTarget)?.getProperties?.() ?? []; } catch { props = []; }
9129
- for (const ps of props) if (propEligible(ps)) noteProp(ps, null);
9707
+ for (const ps of props) { envDeclWritten.add(envLocKey(ps)); if (propEligible(ps)) noteProp(ps, null); }
9708
+ // ⟨R935⟩ a const object literal written reflectively: every member of the literal itself is rebound.
9709
+ const tl = envConstLiteral(unwrapArgExpr(reflectTarget));
9710
+ if (tl && ts.isObjectLiteralExpression(tl)) for (const p of tl.properties) {
9711
+ const k = (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) ? envLocKey(checker.getSymbolAtLocation(p.name)) : null;
9712
+ if (k) { propSyms.add(k); note(k, null); }
9713
+ }
9714
+ }
9715
+ // ⟨R935⟩ an ARRAY literal bound by a `const`: each element is a location holding its own value.
9716
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer
9717
+ && (ts.getCombinedNodeFlags(node) & ts.NodeFlags.Const)) {
9718
+ const al = unwrapArgExpr(node.initializer);
9719
+ if (al && ts.isArrayLiteralExpression(al)) {
9720
+ for (const el of al.elements) if (!ts.isSpreadElement(el) && !ts.isOmittedExpression(el)) {
9721
+ propSyms.add(el); note(el, el); envLitElems.set(el, al);
9722
+ }
9723
+ }
9130
9724
  }
9131
9725
  ts.forEachChild(node, collect);
9132
9726
  };
9133
9727
  for (const sf of sources) collect(sf);
9134
- // ⟨R934⟩ the parameter bindings come from the shared PARAMETER PROVENANCE index above.
9135
- for (const [ps, vals] of paramArgs) { paramSyms.add(ps); for (const v of vals) note(ps, v); }
9728
+ // ⟨R935⟩ an array literal's elements are MUST only while every reference to the array is a literal-index READ.
9729
+ if (envLitElems.size) {
9730
+ const arrs = new Set(envLitElems.values());
9731
+ const names = new Set([...arrs].map((a) => a.parent?.name?.text).filter(Boolean));
9732
+ const opened = new Set();
9733
+ const visitRefs = (node) => {
9734
+ if (ts.isIdentifier(node) && names.has(node.text)) {
9735
+ const lit = envConstLiteral(node);
9736
+ if (lit && arrs.has(lit) && !opened.has(lit) && !(lit.parent?.name === node)) {
9737
+ let cur = node;
9738
+ while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent)
9739
+ || ts.isAsExpression(cur.parent) || ts.isSatisfiesExpression?.(cur.parent))) cur = cur.parent;
9740
+ const p = cur.parent;
9741
+ const indexRead = p && ts.isElementAccessExpression(p) && p.expression === cur && p.argumentExpression
9742
+ && ts.isNumericLiteral(p.argumentExpression)
9743
+ && !(p.parent && ts.isBinaryExpression(p.parent) && p.parent.left === p && p.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
9744
+ && !(p.parent && (ts.isDeleteExpression(p.parent) || ts.isPrefixUnaryExpression(p.parent) || ts.isPostfixUnaryExpression(p.parent)))
9745
+ && !isDestructuringAssignTarget(p);
9746
+ const typeOnly = p && (ts.isTypeQueryNode(p) || ts.isExportSpecifier(p));
9747
+ if (!indexRead && !typeOnly) opened.add(lit);
9748
+ }
9749
+ }
9750
+ ts.forEachChild(node, visitRefs);
9751
+ };
9752
+ for (const sf of sources) visitRefs(sf);
9753
+ for (const [el, al] of envLitElems) if (opened.has(al)) note(el, null);
9754
+ }
9755
+ // ⟨R934⟩ the parameter bindings come from the shared PARAMETER PROVENANCE index above. What `collect` noted for
9756
+ // a parameter BEFORE this line is its in-body REASSIGNMENTS (`e = {}`); kept apart, because the two kinds of
9757
+ // binding answer different questions below.
9758
+ const paramAssigns = new Map();
9759
+ for (const [ps, vals] of paramArgs) {
9760
+ paramSyms.add(ps); paramAssigns.set(ps, [...(bindings.get(ps) ?? [])]);
9761
+ for (const v of vals) note(ps, v);
9762
+ }
9136
9763
  const leafIn = (leaf, set, propSet) => isProcessEnvExpr(leaf)
9137
- || (ts.isIdentifier(leaf) && set.has(checker.getSymbolAtLocation(leaf)))
9138
- || (!!propSet && propSet.has(memberSymOf(leaf)));
9764
+ || (ts.isIdentifier(leaf) && set.has(envValueSymAt(leaf))) // ⟨R1075⟩ through an import
9765
+ || (!!propSet && propSet.has(memberSymOf(leaf)) && !(propSet === envPropMustSymbols && envLitDowngraded(leaf)));
9766
+ // ⟨R935⟩ a literal member read through a receiver whose DECLARED member is written elsewhere is MAY at best.
9767
+ function envLitDowngraded(n) {
9768
+ const lm = (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) ? envLiteralMember(n) : null;
9769
+ return !!lm && !!lm.declKey && lm.declKey !== lm.key && envDeclWritten.has(lm.declKey);
9770
+ }
9139
9771
  const hasEnvLeaf = (v, set, propSet) => v === ENV_DESTRUCTURED
9140
9772
  || (!!v && typeof v === "object" && "same" in v ? set.has(v.same) || propSet?.has(v.same) === true
9141
9773
  : (!!v && typeof v === "object" && envValueLeaves(v).some((l) => leafIn(l, set, propSet))));
9142
9774
 
9143
9775
 
9144
- // MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS and
9145
- // PROPERTIES (a property's MAY reads a local's MAY and vice versa). Parameters never join MAY (above).
9776
+ // MAY: least fixpoint of "some binding has a leaf that is process.env or already MAY" — LOCALS, PROPERTIES
9777
+ // and (⟨R934⟩, see MUST below) PARAMETERS: a property's MAY reads a local's MAY and vice versa.
9146
9778
  for (let changed = true, n = 0; changed && n < 64; n++) {
9147
9779
  changed = false;
9148
9780
  for (const [sym, vals] of bindings) {
9149
- if (paramSyms.has(sym)) continue;
9150
9781
  const isProp = propSyms.has(sym);
9151
9782
  const set = isProp ? envPropMaySymbols : envMayAliasSymbols;
9152
9783
  if (!set.has(sym) && vals.some((v) => hasEnvLeaf(v, envMayAliasSymbols, envPropMaySymbols))) { set.add(sym); changed = true; }
@@ -9154,40 +9785,54 @@ const envLocKey = (sym) => sym?.declarations?.[0] ?? null;
9154
9785
  }
9155
9786
  // MUST: least fixpoint (from empty) of "every binding has a leaf that is process.env or already MUST" —
9156
9787
  // least, so a cycle that never touches process.env directly stays MAY (the disclosing side). Locals and
9157
- // properties start from their MAY sets; parameters are tried directly (MUST-only). A parameter whose
9158
- // function escapes is dropped before it can join.
9159
- const paramCands = [...paramSyms].filter((s) => bindings.get(s)?.some((v) => v !== null));
9160
- let batched = false;
9788
+ // properties start from their MAY sets.
9789
+ //
9790
+ // ⟨SOUNDNESS R934, the DIVERGENT half⟩ A PARAMETER IS THE ENVIRONMENT IN ITS READER'S FRAME WHEN *SOME* VISIBLE
9791
+ // CALL SITE HANDS IT THE ENVIRONMENT — not every one. The first cut of R934 required every site, citing pass
9792
+ // 2b's divergent-HOF ruling, and left `readDiv(e) { return e.SECRET }` called as `readDiv(process.env)` and
9793
+ // `readDiv({…})` ABSENT with `deny Env readDiv` exit 0 over a read that printed the planted secret (EXECUTED).
9794
+ // 2b's ruling does not transfer: it is about a CALLBACK, whose effect is another unit's body, so the HOF
9795
+ // performs nothing itself and pooling the callbacks onto it would invent an effect no execution of the HOF's
9796
+ // own code performs. Here the read is in the callee's OWN body, and an execution in which it reads the
9797
+ // environment exists — that is the all-paths merge `envValueLeaves` already charges for `(o ?? process.env).X`.
9798
+ // Pass 2c, in this same file, already answers the WRITE side this way (`envFed` is set by ANY site, and the
9799
+ // writer is charged `Env`), so the read side requiring every site was the same question answered twice.
9800
+ // The price is the one every summary pays: a caller handing the callee `{}` inherits `Env` through the
9801
+ // ordinary edge, exactly as a caller of 2c's `populate` and of an `??`-merging reader already does.
9802
+ // Because SOME suffices, whether the function escapes no longer matters — hidden sites can only add bindings.
9803
+ // An in-body REASSIGNMENT (`e = {}`) is the flow-insensitive rebind the local-alias rule exists for: the
9804
+ // parameter is then MUST only if every reassignment is also the environment, and otherwise MAY, which
9805
+ // discloses `Unknown[env-maybe-read]` on the reader rather than charging over a value that was replaced.
9161
9806
  const mustTry = () => {
9162
9807
  let changed = false;
9163
9808
  for (const sym of envMayAliasSymbols) {
9809
+ if (paramSyms.has(sym)) continue;
9164
9810
  if (!envAliasSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envAliasSymbols.add(sym); changed = true; }
9165
9811
  }
9166
9812
  for (const sym of envPropMaySymbols) {
9167
9813
  if (!envPropMustSymbols.has(sym) && bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) { envPropMustSymbols.add(sym); changed = true; }
9168
9814
  }
9169
- for (const sym of paramCands) {
9815
+ for (const sym of paramSyms) {
9170
9816
  if (envAliasSymbols.has(sym)) continue;
9171
- if (!bindings.get(sym).every((v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols))) continue;
9172
- const fsym = paramOwnerOf.get(sym);
9173
- if (!batched) { batched = true; paramFnEscaped.batch(paramCands.map((p) => paramOwnerOf.get(p)).filter(Boolean)); }
9174
- if (paramFnEscaped(fsym)) continue;
9817
+ const must = (v) => hasEnvLeaf(v, envAliasSymbols, envPropMustSymbols);
9818
+ if (!(paramArgs.get(sym) ?? []).some(must) || !(paramAssigns.get(sym) ?? []).every(must)) continue;
9175
9819
  envAliasSymbols.add(sym); changed = true;
9176
9820
  }
9177
9821
  return changed;
9178
9822
  };
9179
9823
  for (let n = 0; mustTry() && n < 64; n++) { /* to fixpoint */ }
9824
+ envBindingTable = { bindings, memberSymOf, paramSyms }; // ⟨R935⟩ the floor below asks what each location holds
9180
9825
  }
9181
9826
  // True when `id` is an identifier resolving to a confirmed process.env alias local.
9182
9827
  const identIsEnvAlias = (id) => {
9183
9828
  if (!id || !ts.isIdentifier(id)) return false;
9184
- const sym = checker.getSymbolAtLocation(id);
9829
+ const sym = envValueSymAt(id); // ⟨R1075⟩ through an import
9185
9830
  return !!sym && envAliasSymbols.has(sym);
9186
9831
  };
9187
9832
  // MAY-read: a MAY-alias (ever-bound to process.env but reassignable). A superset of the MUST set.
9188
9833
  const identIsEnvMayAlias = (id) => {
9189
9834
  if (!id || !ts.isIdentifier(id)) return false;
9190
- const sym = checker.getSymbolAtLocation(id);
9835
+ const sym = envValueSymAt(id); // ⟨R1075⟩ through an import
9191
9836
  return !!sym && envMayAliasSymbols.has(sym);
9192
9837
  };
9193
9838
  // The env kind of an expression's VALUE: "env" if some leaf is process.env or a MUST-alias, "may" if
@@ -9195,6 +9840,8 @@ const identIsEnvMayAlias = (id) => {
9195
9840
  // ⟨R935⟩ a member reference `o.p` / `o["p"]` whose property is an environment location.
9196
9841
  const envMemberSym = (n) => {
9197
9842
  if (!(envPropMaySymbols.size > 0)) return null;
9843
+ const lm = envLiteralMember(n); // ⟨R935⟩ the object the receiver holds, before the member its type declares
9844
+ if (lm && envPropMaySymbols.has(lm.key)) return lm.key;
9198
9845
  if (ts.isPropertyAccessExpression(n)) return envLocKey(checker.getSymbolAtLocation(n.name));
9199
9846
  if (ts.isElementAccessExpression(n) && n.argumentExpression && ts.isStringLiteralLike(n.argumentExpression))
9200
9847
  return envLocKey(checker.getSymbolAtLocation(n.argumentExpression));
@@ -9203,7 +9850,11 @@ const envMemberSym = (n) => {
9203
9850
  const envPropKind = (n) => {
9204
9851
  const s = envMemberSym(n);
9205
9852
  if (!s || !envPropMaySymbols.has(s)) return null;
9206
- return envPropMustSymbols.has(s) ? "env" : "may";
9853
+ if (!envPropMustSymbols.has(s)) return "may";
9854
+ // ⟨R935⟩ resolved through the receiver: MAY wherever the declared member is written off the literal.
9855
+ const lm = envLiteralMember(n);
9856
+ if (lm && lm.key === s && lm.declKey && lm.declKey !== s && envDeclWritten.has(lm.declKey)) return "may";
9857
+ return "env";
9207
9858
  };
9208
9859
  const envValueKind = (expr) => {
9209
9860
  let kind = null;
@@ -9238,7 +9889,19 @@ const envRefKind = (node) => {
9238
9889
  }
9239
9890
  if (!ts.isIdentifier(node)) return null;
9240
9891
  // ⟨R934⟩ MUST-parameters are in `envAliasSymbols` but never in the MAY set, so the name filter reads both.
9241
- envAliasNames ??= new Set([...envMayAliasSymbols, ...envAliasSymbols].map((s) => s.name));
9892
+ envAliasNames ??= (() => {
9893
+ const names = new Set([...envMayAliasSymbols, ...envAliasSymbols].map((s) => s.name));
9894
+ // ⟨R1075⟩ …and the LOCAL name of every import that resolves to one (`import { myEnv as e }`).
9895
+ const visit = (n) => {
9896
+ if ((ts.isImportSpecifier(n) || ts.isImportClause(n)) && n.name) {
9897
+ const t = envValueSymAt(n.name);
9898
+ if (t && (envAliasSymbols.has(t) || envMayAliasSymbols.has(t))) names.add(n.name.text);
9899
+ }
9900
+ if (!ts.isFunctionLike(n) && !ts.isClassLike(n)) ts.forEachChild(n, visit);
9901
+ };
9902
+ if (names.size) for (const sf of sources) visit(sf);
9903
+ return names;
9904
+ })();
9242
9905
  if (!envAliasNames.has(node.text)) return null;
9243
9906
  const p = node.parent;
9244
9907
  if (!p) return null;
@@ -9251,17 +9914,351 @@ const envRefKind = (node) => {
9251
9914
  if (ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
9252
9915
  if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
9253
9916
  && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return null;
9254
- const sym = ts.isShorthandPropertyAssignment(p) && p.name === node
9917
+ let sym = ts.isShorthandPropertyAssignment(p) && p.name === node
9255
9918
  ? checker.getShorthandAssignmentValueSymbol(p) : checker.getSymbolAtLocation(node);
9919
+ if (sym && (sym.flags & ts.SymbolFlags.Alias)) { try { sym = checker.getAliasedSymbol(sym); } catch { /* unresolved */ } } // ⟨R1075⟩
9256
9920
  if (!sym) return null;
9257
9921
  if (envAliasSymbols.has(sym)) return "env";
9258
9922
  return envMayAliasSymbols.has(sym) ? "may" : null;
9259
9923
  };
9924
+ // ⟨SOUNDNESS R935, the CONTAINER / ITERATION / OPEN-PROPERTY half⟩ A FLOOR UNDER THE LOCATION MODEL, KEYED ON THE
9925
+ // TYPE. Three lanes closed R935 one storage kind at a time — a value-bearing declaration, then a const literal
9926
+ // receiver — and each left the next kind silent: `const m = new Map([["e", process.env]]); m.get("e")!.SECRET`,
9927
+ // a `Set`/`WeakMap`, `for (const x of arr) x.SECRET`, `arr.map((x) => x.SECRET)`, all EXECUTED (`fx35`), each
9928
+ // printed the planted value, each reader ABSENT with `deny Env` and `deny Env Unknown` exit 0. Every one is a value
9929
+ // whose PROVENANCE the binding table cannot enumerate, and the set of such storage kinds is open; a ninth special
9930
+ // case would leave a tenth. What every one of them still carries is node's `NodeJS.ProcessEnv` TYPE, which the
9931
+ // checker propagated from `process.env` through the container's inferred type argument.
9932
+ // So: an expression whose type is `ProcessEnv`, which the location model above did NOT resolve, and whose value
9933
+ // comes from somewhere this pass cannot see into, is POSSIBLY the environment — `Unknown[env-maybe-read]`, the MAY
9934
+ // posture every other location uses. Never `Env`: a `ProcessEnv`-typed object can be a copy or a literal.
9935
+ // "Somewhere this pass cannot see into" is the part that keeps it from re-asking settled questions:
9936
+ // · a PARAMETER of a named function or method is R934's — a caller that hands it the environment is charged at
9937
+ // its own call, and a parameter no visible site feeds is not charged there either. A CALLBACK's parameter
9938
+ // (an arrow or function expression passed as an argument) is not: its caller is the HOF — `Array.map`,
9939
+ // `Set.forEach` — and nothing in the scan ever charges that call.
9940
+ // · a call to a PROJECT function with a body is judged inside the callee, whose `return` is a consumer there.
9941
+ // · a `const`/`let` is opaque only if its initializer is (or it has none); otherwise its initializer is either
9942
+ // resolved or itself floored where it is evaluated.
9943
+ // RESIDUAL, stated: the type is the carrier, so an environment object stored under `any`, `unknown`, `object` or
9944
+ // `Record<…>` (`new Map<string, any>([["e", process.env]])`) is outside the floor.
9945
+ const isProcessEnvType = (t0) => {
9946
+ let t = t0;
9947
+ try { t = checker.getNonNullableType(t0); } catch { /* keep */ }
9948
+ for (const u of t?.isUnion?.() ? t.types : [t]) {
9949
+ const sy = u?.aliasSymbol ?? u?.symbol;
9950
+ if (sy && sy.getName() === "ProcessEnv"
9951
+ && (sy.declarations ?? []).some((d) => !projectFiles.has(path.resolve(d.getSourceFile().fileName)))) return true;
9952
+ }
9953
+ return false;
9954
+ };
9955
+ // Is this ProcessEnv-typed value's provenance OPAQUE to the binding table? A location the table enumerates is opaque
9956
+ // only if one of its bindings is (a `null` binding is a value the table could not name — a reflective or compound
9957
+ // write — except on a plain variable, where it is the `undefined` of a declaration with no initializer).
9958
+ const envFloorOpaque = (e, depth, seen = new Set()) => {
9959
+ const x = unwrapEnvTransparent(e);
9960
+ if (!x || depth > 6) return false;
9961
+ if (ts.isAwaitExpression(x)) return envFloorOpaque(x.expression, depth + 1, seen);
9962
+ if (isProcessEnvExpr(x)) return false;
9963
+ if (ts.isObjectLiteralExpression(x) || ts.isArrayLiteralExpression(x)) return false;
9964
+ let t = null;
9965
+ try { t = checker.getTypeAtLocation(x); } catch { return false; }
9966
+ if (!isProcessEnvType(t)) return false;
9967
+ const tbl = envBindingTable;
9968
+ const viaBindings = (key, nullOpaque) => {
9969
+ if (seen.has(key)) return false;
9970
+ seen.add(key);
9971
+ return (tbl.bindings.get(key) ?? []).some((v) => {
9972
+ if (v === null || v === undefined) return nullOpaque;
9973
+ if (typeof v === "symbol") return false; // ENV_DESTRUCTURED: resolved
9974
+ if (typeof v === "object" && "same" in v) return tbl.bindings.has(v.same) ? viaBindings(v.same, nullOpaque) : false;
9975
+ return envValueLeaves(v).some((l) => envFloorOpaque(l, depth + 1, seen));
9976
+ });
9977
+ };
9978
+ if (ts.isCallExpression(x) || ts.isNewExpression(x)) {
9979
+ let d = null;
9980
+ try { d = checker.getResolvedSignature(x)?.declaration ?? null; } catch { d = null; }
9981
+ return !(d && projectFiles.has(path.resolve(d.getSourceFile().fileName)) && d.body);
9982
+ }
9983
+ if (ts.isPropertyAccessExpression(x) || ts.isElementAccessExpression(x)) {
9984
+ if (envPropKind(x)) return false;
9985
+ // a project GETTER with a body is judged inside it, as a project callee is (its `return` is a consumer there)
9986
+ let ps = null;
9987
+ try { ps = ts.isPropertyAccessExpression(x) ? checker.getSymbolAtLocation(x.name) : null; } catch { ps = null; }
9988
+ if ((ps?.declarations ?? []).some((dd) => ts.isGetAccessorDeclaration(dd) && dd.body && isProjectNode(dd))) return false;
9989
+ const key = tbl ? tbl.memberSymOf(x) : null;
9990
+ if (key && tbl.bindings.has(key)) return viaBindings(key, true);
9991
+ return true; // an interface member, a dynamic index, a dependency's field: the table cannot enumerate it
9992
+ }
9993
+ if (!ts.isIdentifier(x)) return false;
9994
+ const sym = envValueSymAt(x);
9995
+ if (!sym || envAliasSymbols.has(sym) || envMayAliasSymbols.has(sym)) return false;
9996
+ let d = sym.valueDeclaration ?? sym.declarations?.[0];
9997
+ if (!d) return false;
9998
+ while (d && ts.isBindingElement(d)) d = d.parent?.parent; // a destructured name answers as its pattern's root
9999
+ if (!d) return false;
10000
+ if (ts.isParameter(d)) {
10001
+ const f = d.parent;
10002
+ return !!f && (ts.isArrowFunction(f) || ts.isFunctionExpression(f)) && !!f.parent
10003
+ && (ts.isCallExpression(f.parent) || ts.isNewExpression(f.parent)) && (f.parent.arguments ?? []).includes(f);
10004
+ }
10005
+ if (ts.isVariableDeclaration(d)) {
10006
+ if (d.parent?.parent && (ts.isForOfStatement(d.parent.parent) || ts.isForInStatement(d.parent.parent))) return true;
10007
+ if (!ts.isIdentifier(d.name)) return d.initializer ? envValueLeaves(d.initializer).some((l) => envFloorOpaque(l, depth + 1, seen)) : true;
10008
+ if (tbl && tbl.bindings.has(sym)) return viaBindings(sym, false);
10009
+ return d.initializer ? envValueLeaves(d.initializer).some((l) => envFloorOpaque(l, depth + 1, seen)) : true;
10010
+ }
10011
+ return false;
10012
+ };
10013
+ const envTypeFloorKind = (node) => {
10014
+ if (!(ts.isIdentifier(node) || ts.isCallExpression(node) || ts.isPropertyAccessExpression(node)
10015
+ || ts.isElementAccessExpression(node))) return null;
10016
+ const p = node.parent;
10017
+ if (!p) return null;
10018
+ // not a value REFERENCE: a declared name, the member-name side of an access, a write target, a type position
10019
+ if (ts.isIdentifier(node)) {
10020
+ if ((ts.isVariableDeclaration(p) || ts.isParameter(p) || ts.isBindingElement(p) || ts.isPropertyAssignment(p)
10021
+ || ts.isPropertyDeclaration(p) || ts.isFunctionDeclaration(p) || ts.isMethodDeclaration(p)
10022
+ || ts.isPropertySignature(p) || ts.isShorthandPropertyAssignment(p)) && p.name === node) return null;
10023
+ if (ts.isBindingElement(p) && p.propertyName === node) return null;
10024
+ if ((ts.isPropertyAccessExpression(p) || ts.isQualifiedName(p)) && p.name === node) return null;
10025
+ if (ts.isTypeQueryNode(p) || ts.isExportSpecifier(p) || ts.isImportSpecifier(p) || ts.isImportClause(p)
10026
+ || ts.isNamespaceImport(p) || ts.isImportEqualsDeclaration(p) || ts.isExportAssignment(p)) return null;
10027
+ }
10028
+ if (ts.isBinaryExpression(p) && p.left === node && envIsAssignOp(p.operatorToken.kind)) return null;
10029
+ if (ts.isDeleteExpression(p) || ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p))
10030
+ && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken))) return null;
10031
+ if (isDestructuringAssignTarget(node)) return null;
10032
+ // a TYPE position spelled as an expression: `interface E extends NodeJS.ProcessEnv`, `implements …`
10033
+ {
10034
+ let q = node;
10035
+ while (q.parent && ts.isPropertyAccessExpression(q.parent) && q.parent.expression === q) q = q.parent;
10036
+ const ew = q.parent;
10037
+ if (ew && ts.isExpressionWithTypeArguments(ew) && ew.parent && ts.isHeritageClause(ew.parent)
10038
+ && (ew.parent.token === ts.SyntaxKind.ImplementsKeyword || ts.isInterfaceDeclaration(ew.parent.parent))) return null;
10039
+ }
10040
+ // a binding whose initializer is the floored value is judged at the initializer, not again at every use
10041
+ if (!envFloorOpaque(node, 0)) return null;
10042
+ if (ENVLOC_REACH) { const sf = node.getSourceFile(); ENVLOC_REACH(`floor ${ts.SyntaxKind[node.kind]} ${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}`); }
10043
+ return "may";
10044
+ };
9260
10045
  // ⟨R928/R804⟩ Does the CONSUMER of this env value provably touch no key? Climb through what passes the
9261
10046
  // value on unchanged (the transparent wrappers, both arms of `?:`, either side of `??`/`||`, the right of
9262
10047
  // `&&`/`,`) to the node that consumes it, then answer from a CLOSED list of non-reading consumers.
9263
10048
  // Everything not on it charges: that is the direction a list must fail in here (a missing entry is an
9264
10049
  // over-charge on code that already names the environment, never a silence).
10050
+ // ⟨SOUNDNESS R935, the UNTYPED-CONTAINER half⟩ THE ENVIRONMENT STORED WHERE ITS TYPE IS LOST. The ProcessEnv floor
10051
+ // above rides on the type, so `new Map<string, any>([["e", process.env]])`, `rec["e"] = process.env` on a
10052
+ // `Record<string, any>`, `arr.push(process.env)` into an `any[]`, and `objAny.cfg = { env: process.env }` on an `any`
10053
+ // object left every reader of the container ABSENT (EXECUTED, `fxany`: each printed the planted value, `deny Env` and
10054
+ // `deny Env Unknown` exit 0). Narrowed to where the escape is SEEN rather than to every `any` read:
10055
+ // · the CONTAINERS are only those the environment value is actually put into — a builtin collection built with it
10056
+ // (`new Map|Set|WeakMap|Array(…)`) or mutated with it (`set`/`add`/`push`/`unshift`/`splice`), an element or
10057
+ // dynamic-key write (`c[k] = env`, a `Record` index), or a member write onto an `any`/`unknown` object — named by
10058
+ // the root binding of the receiver. An object literal under a static key is a LOCATION the table above already
10059
+ // enumerates, and a call argument (`spawn(cmd, { env })`) is charged at the call and stored nowhere visible;
10060
+ // · a READ discloses only when its receiver is reached FROM such a container through at least one element step
10061
+ // (a member/element read, a method call's result, a `for…of` variable, a callback parameter of a method called on
10062
+ // it, a parameter whose visible call site passes one) — `m.size` on the container itself reads no key — and its
10063
+ // value is not a primitive. MAY (`Unknown[env-maybe-read]`): which element is the environment is not known.
10064
+ const ENV_COLLECTION_CTORS = new Set(["Map", "Set", "WeakMap", "WeakSet", "Array", "WeakRef"]);
10065
+ const ENV_COLLECTION_MUTATORS = new Set(["set", "add", "push", "unshift", "splice", "fill"]);
10066
+ let envTaintedRoots = null;
10067
+ const envRootSym = (e0) => {
10068
+ let e = unwrapEnvTransparent(e0);
10069
+ for (let g = 0; e && g < 16 && (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e) || ts.isCallExpression(e)); g++)
10070
+ e = unwrapEnvTransparent(ts.isCallExpression(e) ? (ts.isPropertyAccessExpression(e.expression) ? e.expression.expression : null) : e.expression);
10071
+ if (!e || !ts.isIdentifier(e)) return null;
10072
+ const sym = envValueSymAt(e);
10073
+ return sym && (sym.flags & (ts.SymbolFlags.Variable | ts.SymbolFlags.Property)) ? sym : null;
10074
+ };
10075
+ const envIsCollectionType = (e) => {
10076
+ let t; try { t = checker.getNonNullableType(checker.getTypeAtLocation(e)); } catch { return false; }
10077
+ const nm = (t?.getSymbol?.() ?? t?.aliasSymbol)?.getName?.();
10078
+ return ["Map", "Set", "WeakMap", "WeakSet", "Array", "ReadonlyArray"].includes(nm) || !!checker.isArrayType?.(t);
10079
+ };
10080
+ const envAnyish = (e) => {
10081
+ let t; try { t = checker.getTypeAtLocation(e); } catch { return false; }
10082
+ if (!t) return false;
10083
+ if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return true;
10084
+ return !!(t.getStringIndexType?.() || t.getNumberIndexType?.());
10085
+ };
10086
+ const computeEnvTaint = () => {
10087
+ const roots = new Set();
10088
+ const seed = (n) => {
10089
+ let cur = n;
10090
+ for (let g = 0; g < 24; g++) {
10091
+ const p = cur.parent;
10092
+ if (!p) return;
10093
+ if ((ts.isParenthesizedExpression(p) || ts.isAsExpression(p) || ts.isTypeAssertionExpression(p) || ts.isNonNullExpression(p)
10094
+ || ts.isSatisfiesExpression?.(p)) && p.expression === cur) { cur = p; continue; }
10095
+ if (ts.isConditionalExpression(p) && p.condition !== cur) { cur = p; continue; }
10096
+ if (ts.isBinaryExpression(p) && (p.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || p.operatorToken.kind === ts.SyntaxKind.BarBarToken)) { cur = p; continue; }
10097
+ if (ts.isArrayLiteralExpression(p) || ts.isSpreadElement(p)) { cur = p; continue; }
10098
+ if ((ts.isPropertyAssignment(p) && p.initializer === cur) || ts.isShorthandPropertyAssignment(p)) {
10099
+ // a STATIC member of an object literal is a location — unless the literal itself lands in a container (climb on)
10100
+ cur = p.parent; continue;
10101
+ }
10102
+ if (ts.isNewExpression(p) && (p.arguments ?? []).includes(cur)) {
10103
+ const c = unwrapEnvTransparent(p.expression);
10104
+ if (ts.isIdentifier(c) && ENV_COLLECTION_CTORS.has(c.text) && identIsGlobal(c)) { cur = p; continue; }
10105
+ return;
10106
+ }
10107
+ if (ts.isCallExpression(p) && (p.arguments ?? []).includes(cur)) {
10108
+ const ce = unwrapEnvTransparent(p.expression);
10109
+ if (ts.isPropertyAccessExpression(ce) && ENV_COLLECTION_MUTATORS.has(ce.name.text) && envIsCollectionType(ce.expression)) {
10110
+ const r = envRootSym(ce.expression); if (r) roots.add(r);
10111
+ }
10112
+ return;
10113
+ }
10114
+ if (ts.isBinaryExpression(p) && p.right === cur && p.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
10115
+ const l = unwrapEnvTransparent(p.left);
10116
+ const dyn = ts.isElementAccessExpression(l) ? (!l.argumentExpression || !ts.isNumericLiteral(l.argumentExpression) || envAnyish(l.expression))
10117
+ : ts.isPropertyAccessExpression(l) && envAnyish(l.expression);
10118
+ if (dyn) { const r = envRootSym(l.expression); if (r) roots.add(r); }
10119
+ return;
10120
+ }
10121
+ if (ts.isVariableDeclaration(p) && p.initializer === cur) {
10122
+ // only a value that went through a CONTAINER on the way (an array/collection) — a bare alias or a static
10123
+ // object literal is the binding table's question
10124
+ if (cur !== n && ts.isIdentifier(p.name) && (ts.isNewExpression(cur) || ts.isArrayLiteralExpression(cur)
10125
+ || (ts.isObjectLiteralExpression(cur) && envAnyish(p.name)))) {
10126
+ const sym = checker.getSymbolAtLocation(p.name); if (sym) roots.add(sym);
10127
+ }
10128
+ return;
10129
+ }
10130
+ return;
10131
+ }
10132
+ };
10133
+ const visit = (n) => {
10134
+ if (envRefKind(n)) seed(n);
10135
+ ts.forEachChild(n, visit);
10136
+ };
10137
+ for (const sf of sources) visit(sf);
10138
+ return roots;
10139
+ };
10140
+ // How many element steps separate `e` from a tainted container (-1: not reached from one).
10141
+ const envFromTainted = (e0, steps, depth, seen) => {
10142
+ const e = unwrapEnvTransparent(e0);
10143
+ if (!e || depth > 8) return -1;
10144
+ if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return envFromTainted(e.expression, steps + 1, depth + 1, seen);
10145
+ if (ts.isCallExpression(e)) {
10146
+ const ce = unwrapEnvTransparent(e.expression);
10147
+ return ts.isPropertyAccessExpression(ce) ? envFromTainted(ce.expression, steps + 1, depth + 1, seen) : -1;
10148
+ }
10149
+ if (!ts.isIdentifier(e)) return -1;
10150
+ const sym = envValueSymAt(e);
10151
+ if (!sym || seen.has(sym)) return -1;
10152
+ seen.add(sym);
10153
+ if (envTaintedRoots.has(sym)) { envTaintHitRoot = sym; return steps; }
10154
+ const d = sym.valueDeclaration;
10155
+ if (!d) return -1;
10156
+ if (ts.isVariableDeclaration(d)) {
10157
+ const loop = d.parent?.parent;
10158
+ if (loop && ts.isForOfStatement(loop)) return ts.isIdentifier(d.name) ? envFromTainted(loop.expression, steps + 1, depth + 1, seen) : -1;
10159
+ return d.initializer && ts.isIdentifier(d.name) ? envFromTainted(d.initializer, steps, depth + 1, seen) : -1;
10160
+ }
10161
+ if (ts.isParameter(d) && ts.isIdentifier(d.name)) {
10162
+ const f = d.parent;
10163
+ if ((ts.isArrowFunction(f) || ts.isFunctionExpression(f)) && f.parent && ts.isCallExpression(f.parent) && f.parent.arguments.includes(f)) {
10164
+ const ce = unwrapEnvTransparent(f.parent.expression);
10165
+ return ts.isPropertyAccessExpression(ce) && f.parameters[0] === d ? envFromTainted(ce.expression, steps + 1, depth + 1, seen) : -1;
10166
+ }
10167
+ let best = -1;
10168
+ for (const a of paramArgs.get(sym) ?? []) {
10169
+ const r = a ? envFromTainted(a, steps, depth + 1, new Set(seen)) : -1;
10170
+ if (r >= 0 && (best < 0 || r > best)) best = r; // the farthest site: MUST only if every reached site is one step
10171
+ }
10172
+ return best;
10173
+ }
10174
+ return -1;
10175
+ };
10176
+ let envTaintHitRoot = null;
10177
+ // ⟨R935⟩ THE RESOLUTION: a container whose every value is the environment and that never escapes. A `const`, not
10178
+ // exported, whose initializer is empty or holds only environment values (`new Map([[k, env]…])`, `[env]`, `{}`), every
10179
+ // mutation stores the environment (`set(k, env)`, `add(env)`, `push(env)`, `c[k] = env`), and every other reference
10180
+ // is a read-only method or element read — then an ELEMENT (exactly one step in) IS the environment, and its member
10181
+ // read is `Env`, not `Unknown`. Anything else stays MAY.
10182
+ const ENV_CONTAINER_READERS = new Set(["get", "has", "at", "find", "findLast", "values", "forEach", "map", "filter", "some",
10183
+ "every", "reduce", "includes", "indexOf", "keys", "entries", "size", "length", "slice", "flatMap"]);
10184
+ const envAllEnvMemo = new Map();
10185
+ const envContainerAllEnv = (sym) => {
10186
+ if (envAllEnvMemo.has(sym)) return envAllEnvMemo.get(sym);
10187
+ envAllEnvMemo.set(sym, false);
10188
+ const d = sym.valueDeclaration;
10189
+ const isEnv = (x) => !!x && envRefKind(unwrapEnvTransparent(x)) === "env";
10190
+ if (!d || !ts.isVariableDeclaration(d) || !ts.isIdentifier(d.name) || !(ts.getCombinedNodeFlags(d) & ts.NodeFlags.Const)
10191
+ || !projectFiles.has(path.resolve(d.getSourceFile().fileName))) return false;
10192
+ const st = d.parent?.parent;
10193
+ if (st && ts.isVariableStatement(st) && st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return false;
10194
+ const init = unwrapEnvTransparent(d.initializer);
10195
+ const litOk = (lit, tuple) => ts.isArrayLiteralExpression(lit) && lit.elements.every((el) => tuple
10196
+ ? (ts.isArrayLiteralExpression(el) && el.elements.length === 2 && isEnv(el.elements[1])) : isEnv(el));
10197
+ let ok = false;
10198
+ if (init && ts.isArrayLiteralExpression(init)) ok = litOk(init, false);
10199
+ else if (init && ts.isObjectLiteralExpression(init)) ok = init.properties.length === 0;
10200
+ else if (init && ts.isNewExpression(init) && ts.isIdentifier(init.expression)) {
10201
+ const a0 = init.arguments?.[0] ? unwrapEnvTransparent(init.arguments[0]) : null;
10202
+ const nm = init.expression.text;
10203
+ ok = !a0 || ((nm === "Map" || nm === "WeakMap") ? litOk(a0, true) : (nm === "Set" || nm === "WeakSet") ? litOk(a0, false) : false);
10204
+ }
10205
+ if (!ok) return false;
10206
+ let clean = true;
10207
+ const visit = (n) => {
10208
+ if (!clean) return;
10209
+ if (ts.isIdentifier(n) && n.text === d.name.text && n !== d.name && envValueSymAt(n) === sym) {
10210
+ let cur = n;
10211
+ while (cur.parent && (ts.isParenthesizedExpression(cur.parent) || ts.isNonNullExpression(cur.parent) || ts.isAsExpression(cur.parent))) cur = cur.parent;
10212
+ const p = cur.parent;
10213
+ if (p && ts.isPropertyAccessExpression(p) && p.expression === cur) {
10214
+ const call = p.parent && ts.isCallExpression(p.parent) && p.parent.expression === p ? p.parent : null;
10215
+ const m = p.name.text;
10216
+ if (call && (m === "set")) clean = isEnv(call.arguments[1]);
10217
+ else if (call && (m === "add" || m === "push" || m === "unshift")) clean = call.arguments.every((x) => isEnv(x));
10218
+ else if (!ENV_CONTAINER_READERS.has(m)) clean = false;
10219
+ } else if (p && ts.isElementAccessExpression(p) && p.expression === cur) {
10220
+ const asg = p.parent && ts.isBinaryExpression(p.parent) && p.parent.left === p ? p.parent : null;
10221
+ if (asg) clean = asg.operatorToken.kind === ts.SyntaxKind.EqualsToken && isEnv(asg.right);
10222
+ } else if (p && ts.isForOfStatement(p) && p.expression === cur) { /* iteration reads */ }
10223
+ else if (p && ts.isTypeQueryNode(p)) { /* type position */ }
10224
+ else clean = false; // passed, returned, aliased, spread: escapes
10225
+ }
10226
+ ts.forEachChild(n, visit);
10227
+ };
10228
+ visit(d.getSourceFile());
10229
+ envAllEnvMemo.set(sym, clean);
10230
+ return clean;
10231
+ };
10232
+ const envTaintKind = (node) => {
10233
+ if (!(ts.isIdentifier(node) || ts.isCallExpression(node) || ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)
10234
+ || ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node))) return null;
10235
+ const p = node.parent;
10236
+ // only the RECEIVER of a member read
10237
+ if (!p || !((ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) && p.expression === node)) return null;
10238
+ if (ts.isBinaryExpression(p.parent) && p.parent.left === p && envIsAssignOp(p.parent.operatorToken.kind)) return null;
10239
+ if (ts.isCallExpression(p.parent) && p.parent.expression === p) return null; // a method call on it, not a key read
10240
+ envTaintedRoots ??= computeEnvTaint();
10241
+ if (!envTaintedRoots.size) return null;
10242
+ let t; try { t = checker.getTypeAtLocation(node); } catch { return null; }
10243
+ if (t && (t.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.BigIntLike
10244
+ | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void))) return null;
10245
+ if (envIsCollectionType(node)) return null; // a nested container is reached, not read
10246
+ // a member the LOCATION model enumerates (a const literal's element at a literal index, a declared property) has
10247
+ // its own answer — env, may, or provably not — and the taint does not overrule it
10248
+ {
10249
+ const u = unwrapEnvTransparent(node);
10250
+ if ((ts.isPropertyAccessExpression(u) || ts.isElementAccessExpression(u)) && envBindingTable) {
10251
+ const key = envBindingTable.memberSymOf(u);
10252
+ if (key && envBindingTable.bindings.has(key)) return null;
10253
+ }
10254
+ }
10255
+ envTaintHitRoot = null;
10256
+ const steps = envFromTainted(node, 0, 0, new Set());
10257
+ if (steps < 1) return null;
10258
+ const must = steps === 1 && envTaintHitRoot && envContainerAllEnv(envTaintHitRoot);
10259
+ if (ENVLOC_REACH) { const sf = node.getSourceFile(); ENVLOC_REACH(`taint ${must ? "env" : "may"} ${path.relative(rootDir, sf.fileName)}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}`); }
10260
+ return must ? "env" : "may";
10261
+ };
9265
10262
  const envConsumerInert = (node) => {
9266
10263
  let cur = node;
9267
10264
  for (;;) {
@@ -9358,7 +10355,7 @@ const ENV_TOUCHING_BUILTIN = new Set([
9358
10355
  // BARE-identifier global builtins that enumerate every key of an object argument (deep-clone reads them all).
9359
10356
  const ENV_TOUCHING_GLOBAL = new Set(["structuredClone"]);
9360
10357
  const identIsGlobal = (id) => // an identifier that is the ambient global (no project-local declaration shadows it)
9361
- !(checker.getSymbolAtLocation(id)?.declarations ?? []).some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
10358
+ !projectShadows(checker.getSymbolAtLocation(id)?.declarations); // ⟨R1074⟩
9362
10359
  // THE `globalThis.` QUALIFIER, WHICH DEFEATED FIVE TEXT-KEYED ARMS AT ONCE (SOUNDNESS row id pending —
9363
10360
  // filed by the coordinator, not invented here). Every arm below
9364
10361
  // recognises a whole-object builtin by the callee's TEXT (`Object.assign`, `Reflect.set`, `Object.keys`),
@@ -9641,6 +10638,12 @@ function visitCalls(node) {
9641
10638
  }
9642
10639
  const sig = checker.getResolvedSignature(node);
9643
10640
  let decl = sig && sig.declaration;
10641
+ // ⟨R1016⟩ a construction of a class expression runs its constructor unit, whatever the signature names.
10642
+ const classExprCtor = rec ? classExprCtorOfNew(node) : null;
10643
+ if (classExprCtor && classExprCtor !== owner) {
10644
+ rec.edges.add(classExprCtor);
10645
+ if (process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH new ${owner} -> ${classExprCtor}`);
10646
+ }
9644
10647
  // ⟨R103⟩ A WRITABLE SLOT IS AN INCOMPLETE CANDIDATE SET — see `openCallSlot`, and see the class-
9645
10648
  // override fan-out below, which is the authority this converges on rather than a second rule: it
9646
10649
  // edges to every candidate it CAN name and adds `Unknown` when the set it enumerated is not
@@ -9660,8 +10663,8 @@ function visitCalls(node) {
9660
10663
  if (!decl) {
9661
10664
  // `new C()` on a class with an IMPLICIT constructor resolves to no declaration — edge to
9662
10665
  // the class's (synthesized) ctor unit via the class identifier before concluding Unknown.
9663
- let edged = false, externalClass = false;
9664
- if (ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) {
10666
+ let edged = !!classExprCtor, externalClass = false; // ⟨R1016⟩
10667
+ if (!edged && ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) {
9665
10668
  const cd = realDecl(checker.getSymbolAtLocation(node.expression));
9666
10669
  const t = cd && nodeName.get(cd);
9667
10670
  if (t) { rec.edges.add(t); edged = true; }
@@ -10131,6 +11134,12 @@ function visitCalls(node) {
10131
11134
  const confAt = isSuperReceiver(confRecv) ? null
10132
11135
  : (localReceiverClass(confRecv) ?? (ts.isClassDeclaration(decl.parent) ? decl.parent : null));
10133
11136
  const confBodies = confAt ? conformerBodies(confAt, decl.name?.getText?.()) : [];
11137
+ // ⟨SOUNDNESS R1061⟩ the receiver's class is a DI token whose substitute the scan cannot name: the body that
11138
+ // runs may be none of the candidates above — disclose, as a dispatch over an open family does.
11139
+ if (confAt && openSubstitutedClasses.has(confAt)) {
11140
+ rec.direct.add("Unknown");
11141
+ rec.why.add(dispatchWhy(`${moduleOf(confAt.getSourceFile())}.${namespacePrefixOf(confAt)}${confAt.name?.getText?.() ?? "<anonymous>"}`, decl.name?.getText?.()));
11142
+ }
10134
11143
  if (allOverrides.length > 0 || confBodies.length > 0) {
10135
11144
  // PRECISION: scope the fan-out to the RECEIVER's static-type subtree. A base-member
10136
11145
  // dispatch on a receiver statically typed as subclass `Cat` can only ever bind to a
@@ -11088,7 +12097,7 @@ function visitCalls(node) {
11088
12097
  // ⟨R934⟩ a DESTRUCTURED parameter that every visible call site hands the environment reads its keys
11089
12098
  // as the frame is entered — the destructuring IS the read, in this function's frame.
11090
12099
  if (ts.isParameter(node) && ts.isObjectBindingPattern(node.name) && envAliasSymbols.has(node)) { markEnv(); ENVLOC_REACH?.("param-pattern env"); }
11091
- const kind = envRefKind(node);
12100
+ const kind = envRefKind(node) ?? envTypeFloorKind(node) ?? envTaintKind(node); // ⟨R935⟩ floor, then untyped containers
11092
12101
  if (kind && ENVLOC_REACH && !envConsumerInert(node)) {
11093
12102
  const vd = ts.isIdentifier(node) ? checker.getSymbolAtLocation(node)?.valueDeclaration : null;
11094
12103
  const where = `${path.relative(process.cwd(), node.getSourceFile().fileName)}:${node.getStart() >= 0 ? node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1 : 0}`;
@@ -11210,8 +12219,9 @@ function visitCalls(node) {
11210
12219
  // `globalThis.process.hrtime.bigint` → "hrtime.bigint", `(global as any).process.send` → "send". Reuses
11211
12220
  // `identIsGlobalProcess` (which handles bare `process` AND `globalThis`/`global.process`, unwrapping casts,
11212
12221
  // and rejects a project-local `const process = {…}` shadow so a pure local method never fabricates Clock/Ipc).
12222
+ // ⟨R1075⟩ …or rooted at a MEMBER imported from node's `process` module: `import { hrtime } from "node:process";
12223
+ // hrtime()` / `hrtime.bigint()` were ABSENT while `process.hrtime()` charged Clock (EXECUTED-shape, `fx75b`).
11213
12224
  const processMemberPath = () => {
11214
- if (!ts.isPropertyAccessExpression(callee)) return null;
11215
12225
  const outer = [];
11216
12226
  let n = callee;
11217
12227
  while (ts.isPropertyAccessExpression(n)) {
@@ -11219,7 +12229,8 @@ function visitCalls(node) {
11219
12229
  outer.push(n.name.text);
11220
12230
  n = n.expression;
11221
12231
  }
11222
- return null;
12232
+ const im = ts.isIdentifier(n) ? processModuleBinding(n) : null;
12233
+ return im && im !== "process" ? [im, ...outer.reverse()].join(".") : null;
11223
12234
  };
11224
12235
  const pmp = processMemberPath();
11225
12236
  // R114 — THE MEMBER SET IS THE SHARED CONSTANT, read on the LAST segment of the path. This arm and
@@ -11290,7 +12301,7 @@ function visitCalls(node) {
11290
12301
  }
11291
12302
  // Not an import at all: the ambient global, unless the project declares its own.
11292
12303
  if (callee.text !== "fetch") return false;
11293
- return !(sym0.declarations ?? []).some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
12304
+ return !projectShadows(sym0.declarations); // ⟨R1074⟩
11294
12305
  })())
11295
12306
  geff = "Net";
11296
12307
  // the fully-qualified global fetch — `globalThis.fetch`/`window.fetch`/`self.fetch` — is a
@@ -11380,8 +12391,7 @@ function visitCalls(node) {
11380
12391
  // never a concrete effect. (Found by real-world corpus testing; sibling of the node:vm fix.)
11381
12392
  if (ts.isIdentifier(callee) && callee.text === "require"
11382
12393
  && node.arguments?.length === 1 && !ts.isStringLiteralLike(node.arguments[0])
11383
- && !(checker.getSymbolAtLocation(callee)?.declarations ?? [])
11384
- .some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)))) {
12394
+ && !projectShadows(checker.getSymbolAtLocation(callee)?.declarations)) { // ⟨R1074⟩
11385
12395
  const owner = enclosing(node);
11386
12396
  if (owner) { fns.get(owner).direct.add("Unknown"); fns.get(owner).why.add("reflect:require"); }
11387
12397
  }
@@ -11419,7 +12429,7 @@ function visitCalls(node) {
11419
12429
  try { sym = checker.getAliasedSymbol(sym); } catch { /* unresolved import — treat as external */ }
11420
12430
  }
11421
12431
  const decls = sym?.declarations ?? [];
11422
- return !decls.some((d) => projectFiles.has(path.resolve(d.getSourceFile().fileName)));
12432
+ return !projectShadows(decls); // ⟨R1074⟩
11423
12433
  };
11424
12434
  const fromCreateRequire = (expr) => {
11425
12435
  if (!ts.isIdentifier(expr)) return false;
@@ -12112,14 +13122,27 @@ for (const sf of sources) visitCalls(sf);
12112
13122
  add(staticBlockQual(m), "static"); // R815: a static field initialiser shares the block's unit
12113
13123
  // R815: the `extends <expr>` unit — run by the definer (here) and reached by every construction (`super`).
12114
13124
  const ctorQ = nodeName.get(cls);
12115
- const herQ = ctorQ && ctorQ.endsWith(".constructor") ? ctorQ.replace(/\.constructor$/, ".<heritage>") : null;
13125
+ const herQ = heritageQualOf(ctorQ);
12116
13126
  // Wired only when the expression attributed something: a bare `extends Base` mints the unit (a reference was
12117
13127
  // attributed) and carries nothing, and an edge to an empty unit is a `calls` entry on every subclass for no answer.
12118
13128
  const herR = herQ && fns.get(herQ);
12119
13129
  if (herR && ["direct", "edges", "why", "blind", "hosts", "paths", "cmds", "incomplete", "dispatch", "tables"]
12120
13130
  .some((k) => herR[k]?.size)) { add(herQ, "heritage"); fns.get(ctorQ)?.edges.add(herQ); }
13131
+ // ⟨R1016⟩ the containment edge to a class expression's constructor, unless its value cannot escape a `new`.
13132
+ // Asked from the class itself rather than its parent, so a `static X = class {…}` lands on `<static-init>`.
13133
+ const ceq = ts.isClassExpression(cls) ? classExprCtorQual.get(cls) : null;
13134
+ if (ceq && classExprEscapes(cls)) {
13135
+ const cfrom = enclosing(cls);
13136
+ const crec = cfrom && cfrom !== ceq ? fns.get(cfrom) : null;
13137
+ if (crec && !crec.edges.has(ceq)) { crec.edges.add(ceq); hit("classexpr"); }
13138
+ } else if (ceq && process.env.CANDOR_R1016_REACH) console.error(`R1016-REACH contained-dropped ${ceq}`);
12121
13139
  if (!targets.length) continue;
12122
- const from = enclosing(cls.parent);
13140
+ // ⟨SOUNDNESS R1060⟩ a class EXPRESSION is asked from itself, as the R1016 edge above is: climbing from its PARENT
13141
+ // starts at a `static X = class {…}` property with no child in hand, so R815's static-initialiser rule never
13142
+ // matched and the inner class's definition-time work was wired from the OUTER class's constructor — `new HB()`
13143
+ // charged for a `static {}` block that runs once, at import (EXECUTED), and the module that runs it silent.
13144
+ // A declaration is still asked from its parent: from itself, `nodeName` would answer its own constructor.
13145
+ const from = ts.isClassExpression(cls) ? enclosing(cls) : enclosing(cls.parent);
12123
13146
  const rec = from && fns.get(from);
12124
13147
  if (!rec) continue;
12125
13148
  for (const [q, k] of targets) if (q !== from && !rec.edges.has(q)) { rec.edges.add(q); hit(k); }