candor-ts 0.39.0 → 0.39.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scan.mjs +295 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.39.0",
3
+ "version": "0.39.2",
4
4
  "mcpName": "io.github.tombaldwin/candor",
5
5
  "description": "candor for TypeScript — per-function side effects, transitively, with a policy gate (candor-spec 0.39)",
6
6
  "type": "module",
package/scan.mjs CHANGED
@@ -242,7 +242,29 @@ const noteRefusalTarget = (t) => { if (refusalTarget === null) refusalTarget =
242
242
  const writeRefusalMarker = (why) => {
243
243
  if (!refusalPrefix) return;
244
244
  try {
245
- fs.mkdirSync(path.dirname(refusalPrefix), { recursive: true });
245
+ // ⟨SOUNDNESS R520⟩ THE MARKER MAY CREATE ITS OWN `.candor`, AND NOT A TREE ABOVE IT.
246
+ //
247
+ // `recursive: true` made every refusal a `mkdir -p` of the prefix's WHOLE path, and that prefix is
248
+ // derived from the first bare token — so `candor-ts nonexistent-target /also-bogus` exited 2
249
+ // (correctly) and left `./nonexistent-target/.candor/report.refused.json` in the operator's cwd: a
250
+ // directory tree named after a TYPO. Found as litter in the candor umbrella's own checkout, by no
251
+ // gate. candor-java and candor-swift create nothing here, so this is two engines disagreeing with
252
+ // two and no clause has to move.
253
+ //
254
+ // The distinction the recursive form missed: ⟨0.28⟩'s fail-closed sink exists to REPLACE a previous
255
+ // run's document, so a later reader cannot mistake a stale report for a current one. A target that
256
+ // does not exist never held a report, so the write has nothing to fail closed OVER — it is pure
257
+ // side effect. Where the target DOES exist the marker still lands, `.candor` and all: CLI-1b's own
258
+ // control asserts that on a FRESH project, which is why this is not a blanket `existsSync` on the
259
+ // dirname — that spelling would mute the marker on exactly the case the rung is FOR.
260
+ //
261
+ // A bare non-recursive `mkdirSync` would encode the same rule (ENOENT into the catch below), but it
262
+ // would also throw EEXIST on the ordinary path, so both conditions are spelled out instead.
263
+ const markerDir = path.dirname(refusalPrefix);
264
+ if (!fs.existsSync(markerDir)) {
265
+ if (!fs.existsSync(path.dirname(markerDir))) return; // nothing to fail closed OVER
266
+ fs.mkdirSync(markerDir);
267
+ }
246
268
  fs.writeFileSync(`${refusalPrefix}.refused.json`, JSON.stringify({
247
269
  candor: { spec: SPEC_VERSION }, refused: true,
248
270
  prefix: refusalPrefix, target: refusalTarget ?? ".", reason: String(why),
@@ -4582,11 +4604,38 @@ function contextualInterfaceDeclsFor(node, depth = 0) {
4582
4604
  }
4583
4605
  return direct;
4584
4606
  }
4607
+ // ⟨R64⟩'s unit local, spelled ONCE. `ownedByDecoratorArgument` below and `decoratorArgUnit` itself are
4608
+ // its only two readers; a second literal spelling is a second thing to keep in step. Declared above
4609
+ // `walkStructural` rather than beside `decoratorArgUnit`, because that walk RUNS at this point in module
4610
+ // evaluation and a `const` declared further down would be in its temporal dead zone.
4611
+ const DECORATOR_ARG_LOCAL = "<decorator-arg>@";
4612
+ /** ⟨SOUNDNESS R531, conformance PART 82⟩ Is this node's attribution owner ⟨R64⟩'s synthesized
4613
+ * decorator-ARGUMENT unit? Asks `enclosing` — the authority — rather than re-deriving "am I inside a
4614
+ * decorator argument" from the AST: two paths computing one fact is how this file has been bitten
4615
+ * before, and `enclosing`'s own R64 climb is subtler than any restatement of it (it distinguishes an
4616
+ * effect embedded in the argument DATA from an unresolvable decorator FACTORY, and `prev`-vs-`p`
4617
+ * decides which). `enclosing` may CREATE that unit as a side effect; it is dropped from `functions[]`
4618
+ * when nothing effectful lands in it, which PART 82's own `ctrllit`/`ctrlpure` arms assert. */
4619
+ function ownedByDecoratorArgument(node) {
4620
+ const owner = fns.get(enclosing(node));
4621
+ return !!owner && typeof owner.local === "string" && owner.local.startsWith(DECORATOR_ARG_LOCAL);
4622
+ }
4585
4623
  // Mint (or ALIAS to an existing unit) every member of a structural implementor `container`
4586
4624
  // (ObjectLiteralExpression's `.properties` or a ClassExpression's `.members` — both walked generically,
4587
4625
  // same shape as the nominal branch's own `cls.members`). Position-keyed, like `decoratorArgUnit` /
4588
4626
  // `staticBlockUnit` above — two structural implementors in one file must not collide.
4589
- function mintStructuralMembers(container) {
4627
+ // ⟨SOUNDNESS R531⟩ `bodiesOnly` — mint the members whose BODY is written here, and take no position on
4628
+ // the members that merely REFERENCE a function declared elsewhere. The alias arm at the bottom of this
4629
+ // function sets `nodeName` to an EXISTING unit, which makes `enclosing()` stop at that property; that is
4630
+ // right when the literal is a structural IMPLEMENTOR (a dispatch on the abstraction has to land
4631
+ // somewhere) and wrong for a bare literal in any other position. MEASURED, and it is not hypothetical:
4632
+ // `({ count: c.count } = { count: 7 })` parses as an ObjectLiteralExpression in DESTRUCTURING-TARGET
4633
+ // position, its `count: c.count` alias resolves to the accessor unit `C.count`, and the setter
4634
+ // invocation the assignment performs was then attributed to the ACCESSOR instead of to the function
4635
+ // containing the assignment — test.mjs `[32]` went red on the first build of this fix, which is the
4636
+ // only reason the arm is split. Body-minting cannot do that: a unit minted from a body it owns moves
4637
+ // no effect that was not already inside it.
4638
+ function mintStructuralMembers(container, bodiesOnly = false) {
4590
4639
  const sf = container.getSourceFile();
4591
4640
  const mod = moduleOf(sf);
4592
4641
  const members = container.properties ?? container.members ?? [];
@@ -4612,6 +4661,7 @@ function mintStructuralMembers(container) {
4612
4661
  // result, a conditional, an opaque holder), is left UNRESOLVED — `nodeName.has(prop)` stays false, so
4613
4662
  // the dispatch site's `allResolved` gate reads this implementor as incomplete and forces Unknown: the
4614
4663
  // disjunction's (b) arm, never silence.
4664
+ if (bodiesOnly) continue; // ⟨R531⟩ see the header — aliasing is an implementor's privilege, not a literal's
4615
4665
  const bound = unwrapBind(init);
4616
4666
  const ref = bound ? bound.ref : ((ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) ? init : null);
4617
4667
  if (ref) {
@@ -4621,6 +4671,25 @@ function mintStructuralMembers(container) {
4621
4671
  }
4622
4672
  }
4623
4673
  }
4674
+ /** ⟨SOUNDNESS R531⟩ THE MINTED UNIT IS KEYED ON THE PROPERTY, AND RESOLUTION ARRIVES AT THE
4675
+ * INITIALIZER. `mintStructuralMembers` sets `nodeName` on the PropertyAssignment/PropertyDeclaration,
4676
+ * but `realDecl` unwraps `a.roll` straight to the ArrowFunction that initialises it — so a plain
4677
+ * `nodeName.get(decl)` misses by exactly one node. Spelled ONCE, because ⟨R531b⟩'s pre-pass has to ask
4678
+ * the SAME question the call walk asks ("can this decl already be resolved to a unit?") and two
4679
+ * spellings of one fact is how this file has been bitten before (§G). MEASURED: the first build of
4680
+ * R531b asked only `nodeName.has(arrow)` and so minted a SECOND unit over a body R531 had already
4681
+ * minted — bson's `src.extended_json.serializeDocument` went from calling `<structural>@10049.Binary`
4682
+ * to calling `<callable>@10057`, one body through two units, which is exactly the double count
4683
+ * conformance PART 82 pins. Narrow by construction: only an arrow/fn-expression sitting DIRECTLY in a
4684
+ * property, and only a `nodeName` entry that already exists — no unit is invented here. */
4685
+ function callTargetUnit(decl) {
4686
+ const direct = nodeName.get(decl);
4687
+ if (direct) return direct;
4688
+ if ((ts.isArrowFunction(decl) || ts.isFunctionExpression(decl)) && decl.parent
4689
+ && (ts.isPropertyAssignment(decl.parent) || ts.isPropertyDeclaration(decl.parent)))
4690
+ return nodeName.get(decl.parent);
4691
+ return undefined;
4692
+ }
4624
4693
  function mintPositionalStructuralUnit(mod, sf, prop, name) {
4625
4694
  const qual = `${mod}.<structural>@${prop.getStart()}.${name}`;
4626
4695
  if (!fns.has(qual)) {
@@ -4632,6 +4701,50 @@ function mintPositionalStructuralUnit(mod, sf, prop, name) {
4632
4701
  endLine: sf.getLineAndCharacterOfPosition(prop.getEnd()).line + 1 });
4633
4702
  }
4634
4703
  nodeName.set(prop, qual);
4704
+ // ⟨SOUNDNESS R519⟩ CONTAINMENT. Minting is an ADDITION, never a MOVE.
4705
+ //
4706
+ // `enclosing()` returns the FIRST ancestor carrying a `nodeName`, so the line above — which exists to
4707
+ // give the member an addressable unit a dispatch can land on — also silently STOPS the member's body
4708
+ // being charged to the function that lexically contains it. Whenever no in-scan dispatch resolves to
4709
+ // the minted unit, the effects then sit in a unit reachable from nothing and the enclosing function
4710
+ // reads PURE: SPEC §2 rule 3's positive purity claim over code this engine read and understood.
4711
+ //
4712
+ // Nothing in a producer package need ever dispatch on a DEPENDENCY's abstraction — R512's own measured
4713
+ // shape is a package that merely SUPPLIES an implementor — and the orphaning is not confined to that
4714
+ // case: a union arm's type-literal member, an index-signature interface's member, and any member the
4715
+ // matched interface does not declare are unreachable by name in ANY scan. MEASURED, v0.38.3 vs the
4716
+ // released v0.39.0, one package, no chain, no unscanned dispatcher: ten spellings went `['Rand']` ->
4717
+ // ABSENT with `deny Rand` exit 1 -> exit 0 (the R519 block in test.mjs, generated from one template).
4718
+ //
4719
+ // THE CONDITION IS UNCONDITIONAL, and that is a decision, not an omission. The obvious narrowing —
4720
+ // "charge unless the member is provably dispatched in-scan" — is UNSOUND for this property: a
4721
+ // dispatch elsewhere in the scan charges the DISPATCHING unit, never the enclosing one, so the
4722
+ // containing function would still read pure over a body written inside it. MEASURED: a function that
4723
+ // builds a class expression and calls its `size` two lines later still lost that `Rand` on v0.39.0.
4724
+ // The exception would also remove charges v0.38.3 made, which is the one direction a fix for a silent
4725
+ // under-report may not move in.
4726
+ //
4727
+ // FAILURE DIRECTION: OVER-charge. A function that builds an implementor it never runs is charged for
4728
+ // what that implementor would do. That is the same over-approximation this engine already makes for
4729
+ // EVERY closure it does not mint — an untyped `{ run(){ … } }` in the same position has always been
4730
+ // charged to its enclosing function (asserted as `viaUntyped` in the R519 block) — so the fix removes
4731
+ // an inconsistency rather than introducing an approximation.
4732
+ //
4733
+ // ALIASES ARE DELIBERATELY EXCLUDED: `mintStructuralMembers`'s `go: other.method` arm sets `nodeName`
4734
+ // to an EXISTING unit and never reaches here. That body is not lexically contained, referencing a
4735
+ // function is not calling it, and v0.38.3 charged nothing for it either — charging it would be a
4736
+ // fabrication introduced in the name of closing an under-report.
4737
+ const owner = enclosing(prop.parent);
4738
+ if (owner && owner !== qual) {
4739
+ fns.get(owner)?.edges.add(qual);
4740
+ // REACH PROBE, env-gated and left in place deliberately. A byte-identical A/B over a corpus that
4741
+ // cannot reach a changed branch is the most flattering number available and the least informative,
4742
+ // and it has been mistaken for evidence four times in this family. `CANDOR_R519_REACH=1` makes this
4743
+ // branch announce itself on stderr so `bin/corpus-ab.py --mark` can COUNT the hits rather than
4744
+ // anyone inferring them afterwards by grep. Off by default: no output, one env read per minted
4745
+ // member.
4746
+ if (process.env.CANDOR_R519_REACH) console.error(`R519-REACH ${qual} <- ${owner}`);
4747
+ }
4635
4748
  }
4636
4749
  for (const sf of sources) {
4637
4750
  (function walkStructural(node) {
@@ -4642,11 +4755,67 @@ for (const sf of sources) {
4642
4755
  // never structurally satisfy an interface with any member on its own.
4643
4756
  if (ts.isObjectLiteralExpression(node) && node.properties.length > 0) {
4644
4757
  const decls = contextualInterfaceDeclsFor(node);
4645
- if (decls.local.length || decls.foreign.length) {
4646
- for (const d of decls.local) registerStructuralImpl(d, node);
4647
- for (const d of decls.foreign) registerForeignStructuralImpl(d, node);
4648
- mintStructuralMembers(node);
4649
- }
4758
+ for (const d of decls.local) registerStructuralImpl(d, node);
4759
+ for (const d of decls.foreign) registerForeignStructuralImpl(d, node);
4760
+ // ⟨SOUNDNESS R531⟩ MINT THE MEMBERS OF EVERY OBJECT LITERAL, not only of a contextually-typed one.
4761
+ //
4762
+ // Minting used to be gated on the literal matching some interface, because minting existed only to
4763
+ // give a DISPATCH somewhere to land. But a minted unit is also the only thing a DIRECT call on the
4764
+ // literal — `const u = { roll: (n) => Math.random() }; u.roll(n)` — can resolve to, and an untyped
4765
+ // literal never reached here. The resolution then landed on the ARROW ITSELF, which the call site
4766
+ // waves through as "already walked lexically" (see the `isArrowFunction` arm in the call walk): true
4767
+ // of the arrow's LEXICAL owner, false of the CALLER. So the caller vanished from `functions[]`
4768
+ // entirely — SPEC §2 rule 3's positive purity claim over a body this engine had read and charged
4769
+ // one unit over.
4770
+ //
4771
+ // MEASURED at v0.38.3, v0.39.0, v0.39.1 and HEAD alike (so NOT an R519 regression): with
4772
+ // `const a = { roll: (n: number) => Math.floor(Math.random() * n) }`, `function A1(n) { return
4773
+ // a.roll(n) }` is ABSENT from `functions[]` and `deny Rand src.index.A1` exits 0 while
4774
+ // `deny Rand src.index.A8` — the same body reached through a plain function — exits 1. Seven
4775
+ // spellings: arrow and function-expression initializers, `as const`, a nested two-hop literal, an
4776
+ // exported literal, a factory-returned literal, and the untyped-parameter caller.
4777
+ //
4778
+ // THE SPELLING ASYMMETRY IS THE TELL, and it is why this was invisible: `{ roll(n){…} }` — method
4779
+ // SHORTHAND on the very same untyped literal — is a MethodDeclaration, not an arrow, so it misses
4780
+ // that arm and discloses an honest `Unknown[callback:roll]`. One literal, two spellings, one loud
4781
+ // and one silent.
4782
+ //
4783
+ // MINTING IS SAFE TO WIDEN ONLY BECAUSE ⟨R519⟩ MADE IT ADDITIVE. Before `mintPositionalStructuralUnit`
4784
+ // gained its containment edge, extending minting to every literal would have MOVED the body of every
4785
+ // `{ onClick: () => … }` out of its enclosing function — R519's own defect, multiplied. The
4786
+ // containment edge is what makes this monotone-up: the enclosing unit keeps the charge, and the
4787
+ // caller gains a unit to resolve to.
4788
+ //
4789
+ // FAILURE DIRECTION: PRECISION-POSITIVE, not over-charge. This resolves rather than hedges — the
4790
+ // method-shorthand spelling goes `Unknown` -> the concrete effect, and a PURE member's caller stays
4791
+ // ABSENT (asserted as the `P1`/pure twin in the R531 block). The rejected alternative was to disclose
4792
+ // `Unknown` at the call site whenever the arrow is not lexically inside the caller; it closes the
4793
+ // same silence and was MEASURED to charge `Unknown` to a caller of `{ twice: (n) => n * 2 }`, which
4794
+ // is closing a silence by flooding the other channel — the trade ⟨0.39⟩'s cost model forbids.
4795
+ //
4796
+ // ONE POSITION IS EXCLUDED, AND CONFORMANCE PART 82 IS WHY. ⟨R64⟩ already mints a unit for a
4797
+ // closure embedded directly in a DECORATOR'S ARGUMENT DATA — `@Factory({ init: () => { … } })` is
4798
+ // the literal example in `decoratorArgUnit`'s own docstring — and that unit exists precisely
4799
+ // because there is no declaration anywhere else for the body to land on. Minting the property as
4800
+ // well splits one non-callable piece of syntax into TWO reported units, and ⟨R519⟩'s containment
4801
+ // edge then feeds the effect back up, so `deny Fs` scores the same `readFileSync` twice:
4802
+ // PART 82 `defect-shape2-argclosure-gate` went `notok count:1` -> `notok count:2`. Fails closed
4803
+ // either way, so it is a double count and not a soundness regression — but a count the family has
4804
+ // pinned four-way is a contract.
4805
+ //
4806
+ // THE BOUND IS "NOT A BINDING SITE", NOT "MAKE PART 82 PASS". A decorator argument is an anonymous
4807
+ // expression: TypeScript gives no way to name that literal, so no call site anywhere can ever
4808
+ // resolve `init`, and the unit R531 mints to be resolved AGAINST can never be reached. Skipping it
4809
+ // costs nothing and returns this position to exactly its pre-R531 behaviour, which was never
4810
+ // silent — the body is charged to `<decorator-arg>@N` and the gate fires.
4811
+ //
4812
+ // THE NEIGHBOURS WERE CHECKED RATHER THAN ASSUMED, because an audit drawn around its own trigger
4813
+ // is this project's most expensive habit. The other synthetic attribution units are `moduleUnit`
4814
+ // and `staticBlockUnit`, and BOTH are real binding sites: `static { const u = { roll: () => … };
4815
+ // u.roll() }` resolves, and so does the module-scope form that R531 was filed for. They are NOT
4816
+ // excluded, and the R531 block asserts both still charge their caller.
4817
+ const bodiesOnly = !(decls.local.length || decls.foreign.length);
4818
+ if (!(bodiesOnly && ownedByDecoratorArgument(node))) mintStructuralMembers(node, bodiesOnly);
4650
4819
  } else if (ts.isClassExpression(node)) {
4651
4820
  // Explicit `implements` on an anonymous/named class EXPRESSION — the nominal branch above only
4652
4821
  // ever visits `ts.isClassDeclaration`, so `held = new (class implements Task { go(){…} })()`
@@ -5340,7 +5509,7 @@ function staticBlockUnit(node) {
5340
5509
  function decoratorArgUnit(callNode) {
5341
5510
  const sf = callNode.getSourceFile();
5342
5511
  const mod = moduleOf(sf);
5343
- const local = `<decorator-arg>@${callNode.getStart()}`;
5512
+ const local = `${DECORATOR_ARG_LOCAL}${callNode.getStart()}`;
5344
5513
  const qual = `${mod}.${local}`;
5345
5514
  let rec = fns.get(qual);
5346
5515
  if (!rec) {
@@ -7237,7 +7406,17 @@ function visitCalls(node) {
7237
7406
  }
7238
7407
  }
7239
7408
  if (mod === "<local>") {
7240
- const targetName = nodeName.get(decl);
7409
+ // ⟨SOUNDNESS R531⟩ the property mint is keyed one node ABOVE the arrow resolution lands on;
7410
+ // `callTargetUnit` is that lookup, spelled once and shared with ⟨R531b⟩'s pre-pass (see its
7411
+ // docstring). Widening the mint without this line leaves the sin open — measured: the R531
7412
+ // arms stayed ABSENT — so the two halves are one fix.
7413
+ const mintedOnProp = nodeName.get(decl) ? undefined : callTargetUnit(decl);
7414
+ // REACH PROBE, env-gated, same contract as ⟨R519⟩'s above: a byte-identical A/B over a corpus
7415
+ // that cannot reach the changed branch is the most flattering number available and the least
7416
+ // informative. `CANDOR_R531_REACH=1` makes THIS branch — the one that turns a silent caller
7417
+ // into a charged one — announce itself so `bin/corpus-ab.py --mark` can count it.
7418
+ if (mintedOnProp && process.env.CANDOR_R531_REACH) console.error(`R531-REACH ${owner} -> ${mintedOnProp}`);
7419
+ const targetName = nodeName.get(decl) ?? mintedOnProp;
7241
7420
  if (targetName) {
7242
7421
  rec.edges.add(targetName); // (EDGE) — cross-FILE edges resolve the same way
7243
7422
  // Class-CHA fan-out: resolution landed on a base-class member that LOCAL subclasses
@@ -8834,6 +9013,113 @@ function visitCalls(node) {
8834
9013
  }
8835
9014
  ts.forEachChild(node, visitCalls);
8836
9015
  }
9016
+ /** ⟨SOUNDNESS R531b⟩ AN ARROW A CALL RESOLVES TO, THAT NO BINDING SITE EVER NAMED.
9017
+ *
9018
+ * ⟨R531⟩ closed the OBJECT-LITERAL-PROPERTY route by minting the property. This is the same silence one
9019
+ * construct over, and it has no property to key on: `const c = (() => (n: number) => Math.random()*n)();
9020
+ * function C(n){ return c(n) }` — the checker resolves `c(n)` to the ARROW returned by the IIFE, that
9021
+ * arrow is nobody's initializer, so `nodeName` has no entry and the call walk's last `else if` waves it
9022
+ * through on the grounds that "its body is visible and already walked lexically". That is true of the
9023
+ * arrow's LEXICAL owner (here the module initializer) and FALSE of the caller, which then vanishes from
9024
+ * `functions[]` entirely — SPEC §2 rule 3's positive purity claim over a body this engine read and
9025
+ * charged one unit over. MEASURED byte-identical on published v0.38.3, v0.39.0, v0.39.1 and HEAD, so
9026
+ * pre-existing, not an R519/R531 regression; `deny Rand src.lib.C1` exits 0 while the same body reached
9027
+ * through a plain `const c4 = (n) => …` exits 1.
9028
+ *
9029
+ * THE BOUNDARY IS THE WAIVER'S OWN CLAIM, NOT A LIST OF SPELLINGS. The audit was deliberately not drawn
9030
+ * around the IIFE that triggered it: a sweep of twenty-three spellings on the published engine found the
9031
+ * identical silence in EIGHT — IIFE→arrow, factory→arrow, IIFE→function-expression, a ternary selecting
9032
+ * between two arrows, `.bind()`, an array-literal element indexed at the call site, an arrow returned
9033
+ * from a class method, and an arrow round-tripped through a generic identity `id<T>(x: T): T`. They are
9034
+ * one defect, because the engine has one waiver, and the general property is "resolution landed on an
9035
+ * arrow/function-expression node that no binding site minted". So the condition here is that property —
9036
+ * every spelling that produces it is covered, including ones nobody has written down yet.
9037
+ *
9038
+ * WHEN THE WAIVER IS TRUE WE DO NOTHING. `enclosing(fn.parent)` is the unit the arrow's body is ALREADY
9039
+ * charged to; when that is the caller's own unit, the caller really has the effects and there is no
9040
+ * silence to close — a plain `(() => { … })()` IIFE inside a function is exactly this, and minting it
9041
+ * would add a report row for no gain. Skipping is safe in the direction that matters: the skipped case is
9042
+ * the one where the pre-fix engine was already correct.
9043
+ *
9044
+ * (a) MINT, NOT (b) DISCLOSE `Unknown` — and (b) was BUILT AND RUN for this route rather than inherited
9045
+ * from R531's measurement of a different one. The decisive result is not the flood, it is that (b) does
9046
+ * not close the gate: `Unknown` is not `Rand`, so `deny Rand C1` stays at EXIT 0 over a real
9047
+ * `Math.random()` — measured on this file's own R531b fixture. The flood is real too, measured on the
9048
+ * 53-entry npm corpus: (b) recovered ZERO concrete effects, charged `Unknown` to 63 previously-absent
9049
+ * callers of which 24 resolve to targets this engine can SEE are pure (axios's `isStream`, `isThenable`,
9050
+ * `toArray`, `isSpecCompliantForm`), and added a spurious reason to 202 existing rows. Minting RESOLVES
9051
+ * instead: a pure target leaves the caller absent exactly as before (asserted by the R531b PRECISION
9052
+ * arms), an effectful one charges the concrete effect and the scoped gate fires.
9053
+ *
9054
+ * MINTING IS ADDITIVE ONLY BECAUSE OF ⟨R519⟩'s CONTAINMENT EDGE, AND THAT STILL HOLDS FOR A NON-PROPERTY
9055
+ * PARENT — it was CHECKED, not assumed, because if it did not hold this would be R519 re-created. R519's
9056
+ * edge is `enclosing(prop.parent) -> minted`; the only thing it needs of the parent is that `enclosing`
9057
+ * can answer for it, and `enclosing` answers for ANY node (it falls through to `moduleUnit` at the
9058
+ * SourceFile). Here the owner is read BEFORE `nodeName.set`, so it is the pre-mint attribution owner by
9059
+ * construction, and the edge gives that owner everything the mint would otherwise have moved out of it.
9060
+ * Asserted directly in test.mjs's R531b block (`viaEnclosing`): the module initializer keeps its `Rand`
9061
+ * after the mint.
9062
+ *
9063
+ * FAILURE DIRECTION: PRECISION-POSITIVE, and additive. No effect is removed from any unit, so no firing
9064
+ * gate can go green; the report gains one row per arrow some call actually resolves to. It is NOT a
9065
+ * blanket mint of every closure — an inline callback (`arr.map(x => …)`) is invoked by the CALLEE, never
9066
+ * resolved by a call in this scan, so it is not a candidate and stays charged to its lexical owner.
9067
+ * ⟨R64⟩'s decorator-argument position, which is what PART 82 pins, is likewise untouched: TypeScript
9068
+ * gives no way to name a decorator argument's closure, so no call resolves to one, and the only closure
9069
+ * there that IS called — an IIFE — takes the waiver-true skip above. PART 82's `count:1` is asserted
9070
+ * unchanged.
9071
+ *
9072
+ * FIXPOINT, because the predicate reads `enclosing` and minting REFINES it. Minting arrow B can change
9073
+ * the owner of a call that resolves to a sibling arrow A from "the shared lexical owner" to B — at which
9074
+ * point A's waiver is no longer true and A must be minted too, or B under-reports A's effects. One pass
9075
+ * in document order is not enough to see that, and the monotonicity (owners only ever get finer, the
9076
+ * candidate set only shrinks) makes the loop terminate in the number of candidates. */
9077
+ function mintCallTargetUnit(fn) {
9078
+ const sf = fn.getSourceFile();
9079
+ const local = `<callable>@${fn.getStart()}`;
9080
+ const qual = `${moduleOf(sf)}.${local}`;
9081
+ const owner = enclosing(fn.parent); // BEFORE the nodeName.set below — the PRE-mint attribution owner
9082
+ if (!fns.has(qual)) {
9083
+ const { line, character } = sf.getLineAndCharacterOfPosition(fn.getStart());
9084
+ fns.set(qual, { local, direct: new Set(), fsKinds: new Set(), edges: new Set(), hosts: new Set(),
9085
+ tables: new Set(), cmds: new Set(), paths: new Set(), blind: new Set(),
9086
+ incomplete: new Set(), dispatch: new Set(), why: new Set(), entry: false,
9087
+ loc: `${path.relative(rootDir, sf.fileName)}:${line + 1}:${character + 1}`,
9088
+ endLine: sf.getLineAndCharacterOfPosition(fn.getEnd()).line + 1 });
9089
+ }
9090
+ nodeName.set(fn, qual);
9091
+ // ⟨R519⟩ CONTAINMENT — minting is an ADDITION, never a MOVE. Same edge, same reason, non-property parent.
9092
+ if (owner && owner !== qual) fns.get(owner)?.edges.add(qual);
9093
+ // REACH PROBE, env-gated, same contract as ⟨R519⟩'s and ⟨R531⟩'s. "CHANGED 0" and "the branch never
9094
+ // ran" print identically; `bin/corpus-ab.py --mark` counts this rather than anyone inferring it later.
9095
+ if (process.env.CANDOR_R531B_REACH) console.error(`R531B-REACH ${owner} -> ${qual}`);
9096
+ return qual;
9097
+ }
9098
+ {
9099
+ const cands = [];
9100
+ for (const sf of sources) (function findAnonCallTargets(n) {
9101
+ if (ts.isCallExpression(n) || ts.isTaggedTemplateExpression(n)) {
9102
+ let d; try { d = checker.getResolvedSignature(n)?.declaration; } catch { d = undefined; }
9103
+ if (d && (ts.isArrowFunction(d) || ts.isFunctionExpression(d)) && d.body
9104
+ && projectFiles.has(path.resolve(d.getSourceFile().fileName)))
9105
+ cands.push({ call: n, fn: d });
9106
+ }
9107
+ ts.forEachChild(n, findAnonCallTargets);
9108
+ })(sf);
9109
+ for (let round = 0; round <= cands.length; round++) {
9110
+ const mint = [];
9111
+ for (const c of cands) {
9112
+ // ASK THE AUTHORITY: anything the call walk can already resolve — including ⟨R531⟩'s
9113
+ // property mint, which is keyed one node ABOVE this arrow — must not be minted again.
9114
+ if (callTargetUnit(c.fn)) continue;
9115
+ if (enclosing(c.fn.parent) === enclosing(c.call)) continue; // the waiver is TRUE here — already walked into the caller
9116
+ mint.push(c.fn);
9117
+ }
9118
+ if (!mint.length) break;
9119
+ mint.sort((a, b) => a.getStart() - b.getStart()); // outer before inner: a nested mint must see its container's unit
9120
+ for (const fn of mint) if (!callTargetUnit(fn)) mintCallTargetUnit(fn);
9121
+ }
9122
+ }
8837
9123
  for (const sf of sources) visitCalls(sf);
8838
9124
 
8839
9125
  // ---- pass 2b: callback-flow resolution (the callback_named move) ----------------------------------