All files / instrumentation AdrProvenance.ts

100% Statements 17/17
100% Branches 7/7
100% Functions 4/4
100% Lines 17/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103                                                                                                                  67x 67x       2195x                       2162x 186x   1976x         480x 480x 480x 942x 942x 261x   681x 681x   480x       517x 517x          
/**
 * Issue #1241: records where an ADR's rule fired during transpilation.
 *
 * Occupancy of the scope-context matrix is derived from source POSITIONS. A
 * diagnostic supplies one; a codegen decision did not, so ADRs governing
 * successful resolution (ADR-016, ADR-057) had fixtures that could never occupy
 * a cell -- the gate read `warn` for seventeen cells that eleven fixtures were
 * already exercising. That is an observability gap, not a coverage gap, and
 * writing more fixtures could not have closed it.
 *
 * Deliberately a plain sink, not a decision-maker: it records that a rule fired
 * at a position and answers nothing about which cell that is. Classification
 * stays in the matrix tooling, walking the parse tree, so provenance cannot
 * drift from the fixture the way a declared cell can.
 *
 * The current file lives here rather than being passed at every call. A
 * recording site is a one-liner inside a decision it is already making, and
 * threading a path through helpers that deliberately take injected deps
 * (TypeGenerationHelper) would have made those helpers know about global state
 * purely to report on themselves.
 *
 * ## #1452, and what this module does NOT satisfy
 *
 * It moved out of `src/transpiler/state/` for box 1, and it does not satisfy
 * box 4 -- "no module reachable from the pipeline holds mutable cross-pass
 * state". It holds two mutable statics, written at 17 `record` sites across
 * 2.1 Analyze and 2.3 Render plus four `beginFile` sites, and read once at the
 * end of the run. That is cross-pass by the box's literal text.
 *
 * Neither alternative is better. Splitting the sink per pass yields three
 * accumulators where there is one, and the only consumer
 * (`scripts/matrix/AdrProvenanceLines.ts`) reads `adr` and `line` and never
 * `sourcePath`, so the attribution a split changes is unobservable to the only
 * gate watching it. Threading an instance through the call sites contradicts
 * the paragraph above, which is a recorded design decision, not an oversight.
 *
 * The owner's call, taken 2026-09-23: box 4 governs PROGRAM state. A fact about
 * the run is not one, so `src/instrumentation/` is admitted by
 * `docs/architecture/README.md` as a fourth kind of root beside the layers, the
 * shared contracts and the host -- and as the one root that may hold mutable
 * state, because what it accumulates is an observation rather than a fact
 * carried between passes. Nothing downstream branches on it.
 *
 * That exemption is stated, not incidental. `passes-hold-no-mutable-state.test.ts`
 * scans the layers and not this root because instrumentation is not a pass, and
 * the line it rests on is enforced: `instrumentation-cannot-import-a-layer`
 * (`reachable: true`) stops this module reaching back into the thing it reports
 * on. Mutation-checked -- an import of a 1-Analyze module produces twelve
 * violations naming this file.
 *
 * The sibling arrives with the rest of #1452: the toolchain-requirement
 * accumulator, still inside `CodeGenState` today, is the same kind of fact and
 * belongs here too.
 */
import type IRecordedAdrSite from "../types/IRecordedAdrSite";
 
class AdrProvenance {
  private static sites: IRecordedAdrSite[] = [];
  private static currentSourcePath: string | null = null;
 
  /** Attribute subsequent recordings to `sourcePath`. */
  static beginFile(sourcePath: string | null): void {
    this.currentSourcePath = sourcePath;
  }
 
  /**
   * Note that `adr`'s rule fired at `line` of the current file.
   *
   * Silently ignores a call with no current file or a non-positive line: a
   * position that cannot be resolved must not become occupancy for whatever
   * declaration happens to start the file, which is the same reason
   * FixtureContext refuses the synthetic 1:0 placeholder.
   */
  static record(adr: string, line: number | null | undefined): void {
    if (!this.currentSourcePath || line == null || line <= 0) {
      return;
    }
    this.sites.push({ adr, sourcePath: this.currentSourcePath, line });
  }
 
  /** Every site recorded since the last reset, deduplicated. */
  static collect(): readonly IRecordedAdrSite[] {
    const seen = new Set<string>();
    const unique: IRecordedAdrSite[] = [];
    for (const site of this.sites) {
      const key = `${site.adr} ${site.sourcePath} ${site.line}`;
      if (seen.has(key)) {
        continue;
      }
      seen.add(key);
      unique.push(site);
    }
    return unique;
  }
 
  static reset(): void {
    this.sites = [];
    this.currentSourcePath = null;
  }
}
 
export default AdrProvenance;